Piattaforma ITI TECHNOLOGY: monorepo, Medusa, API, sito, area riservata e deploy
Deploy stack su Proxmox / deploy (push) Failing after 3s
Deploy stack su Proxmox / deploy (push) Failing after 3s
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
This commit is contained in:
@@ -0,0 +1,22 @@
|
||||
{
|
||||
"name": "@iti/shared",
|
||||
"version": "0.0.0",
|
||||
"private": true,
|
||||
"description": "Tipi e DTO condivisi tra apps/api, apps/public e apps/private",
|
||||
"type": "module",
|
||||
"main": "./dist/index.js",
|
||||
"types": "./dist/index.d.ts",
|
||||
"exports": {
|
||||
".": {
|
||||
"types": "./dist/index.d.ts",
|
||||
"default": "./dist/index.js"
|
||||
}
|
||||
},
|
||||
"files": [
|
||||
"dist"
|
||||
],
|
||||
"scripts": {
|
||||
"build": "tsc -p tsconfig.json",
|
||||
"clean": "node -e \"require('fs').rmSync('dist',{recursive:true,force:true})\""
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,76 @@
|
||||
/**
|
||||
* Configuratore guidato (prompt 18): tipi della definizione del wizard (GET /configurator/definition)
|
||||
* e del risultato (POST /configurator/evaluate | submit). Nessuna logica: le regole stanno in apps/api.
|
||||
*/
|
||||
|
||||
export const CONFIGURATOR_PATHS = ["acqua", "fotovoltaico", "pompa_calore", "azienda"] as const
|
||||
export type ConfiguratorPath = (typeof CONFIGURATOR_PATHS)[number]
|
||||
|
||||
/** Condizione di visibilità: tutte le condizioni di un elenco devono valere (AND). */
|
||||
export interface ConfiguratorCondition {
|
||||
question: string
|
||||
/** Risposta (scelta singola) tra questi valori. */
|
||||
in?: string[]
|
||||
/** Risposta (scelta multipla) che include questo valore. */
|
||||
includes?: string
|
||||
}
|
||||
|
||||
export interface ConfiguratorOption {
|
||||
value: string
|
||||
label: string
|
||||
help?: string
|
||||
}
|
||||
|
||||
interface QuestionBase {
|
||||
id: string
|
||||
label: string
|
||||
help?: string
|
||||
/** Obbligatoria quando visibile. */
|
||||
required: boolean
|
||||
visibleIf?: ConfiguratorCondition[]
|
||||
}
|
||||
|
||||
export type ConfiguratorQuestion =
|
||||
| (QuestionBase & { type: "single"; options: ConfiguratorOption[] })
|
||||
| (QuestionBase & { type: "multi"; options: ConfiguratorOption[]; minSelected: number })
|
||||
| (QuestionBase & { type: "number"; unit: string; min: number; max: number; step: number })
|
||||
| (QuestionBase & { type: "province" })
|
||||
|
||||
export interface ConfiguratorStep {
|
||||
id: string
|
||||
title: string
|
||||
description?: string
|
||||
visibleIf?: ConfiguratorCondition[]
|
||||
questions: ConfiguratorQuestion[]
|
||||
}
|
||||
|
||||
export interface ConfiguratorDefinition {
|
||||
version: string
|
||||
steps: ConfiguratorStep[]
|
||||
/** Province per le domande di tipo "province" (sigla + nome). */
|
||||
provinces: { code: string; name: string }[]
|
||||
}
|
||||
|
||||
/** Risposte: id domanda → valore (scelta singola e provincia: string; multipla: string[]; numero: number). */
|
||||
export type ConfiguratorAnswers = Record<string, string | number | string[]>
|
||||
|
||||
export interface ConfiguratorResult {
|
||||
/** Versione delle regole usate (per riprodurre il calcolo). */
|
||||
rulesVersion: string
|
||||
path: ConfiguratorPath
|
||||
solution: { code: string; title: string; description: string }
|
||||
size: { value: number; unit: string; label: string } | null
|
||||
/** Prodotti suggeriti (handle Medusa). */
|
||||
products: { handle: string; quantity: number; note: string | null }[]
|
||||
/** Stime in centesimi di euro (IVA inclusa); null se non calcolabili per il percorso. */
|
||||
estimates: {
|
||||
investmentCents: number | null
|
||||
annualSavingsCents: number | null
|
||||
paybackYears: number | null
|
||||
}
|
||||
/** Riepilogo leggibile dei calcoli (etichetta/valore). */
|
||||
details: { label: string; value: string }[]
|
||||
warnings: string[]
|
||||
incentivesNote: string
|
||||
disclaimer: string
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
/**
|
||||
* Valori ammessi per gli stati/enumerazioni di dominio.
|
||||
* Fonte unica: usati da apps/api per gli enum Postgres e dai frontend per select, filtri e badge.
|
||||
*/
|
||||
|
||||
export const INSTALLER_STATUSES = ["pending", "approved", "rejected"] as const
|
||||
export type InstallerStatus = (typeof INSTALLER_STATUSES)[number]
|
||||
|
||||
export const QUOTE_REQUEST_STATUSES = ["new", "contacted", "converted", "closed"] as const
|
||||
export type QuoteRequestStatus = (typeof QUOTE_REQUEST_STATUSES)[number]
|
||||
|
||||
export const QUOTE_STATUSES = ["draft", "sent", "accepted", "rejected", "expired"] as const
|
||||
export type QuoteStatus = (typeof QUOTE_STATUSES)[number]
|
||||
|
||||
export const TICKET_STATUSES = ["open", "in_progress", "waiting_customer", "closed"] as const
|
||||
export type TicketStatus = (typeof TICKET_STATUSES)[number]
|
||||
|
||||
export const TICKET_PRIORITIES = ["low", "normal", "high", "urgent"] as const
|
||||
export type TicketPriority = (typeof TICKET_PRIORITIES)[number]
|
||||
|
||||
export const BLOG_POST_STATUSES = ["draft", "published"] as const
|
||||
export type BlogPostStatus = (typeof BLOG_POST_STATUSES)[number]
|
||||
|
||||
/** Chi può vedere/scaricare un documento: public = anche anonimi. */
|
||||
export const DOCUMENT_VISIBILITIES = ["public", "customer", "installer", "admin"] as const
|
||||
export type DocumentVisibility = (typeof DOCUMENT_VISIBILITIES)[number]
|
||||
|
||||
export const CONTACT_REQUEST_STATUSES = ["new", "in_progress", "closed"] as const
|
||||
export type ContactRequestStatus = (typeof CONTACT_REQUEST_STATUSES)[number]
|
||||
|
||||
export const SHOWROOM_BOOKING_STATUSES = ["requested", "confirmed", "cancelled", "completed"] as const
|
||||
export type ShowroomBookingStatus = (typeof SHOWROOM_BOOKING_STATUSES)[number]
|
||||
|
||||
/** privacy: obbligatorio per ogni form con dati personali; marketing: facoltativo e revocabile. */
|
||||
export const CONSENT_TYPES = ["privacy", "marketing"] as const
|
||||
export type ConsentType = (typeof CONSENT_TYPES)[number]
|
||||
@@ -0,0 +1,445 @@
|
||||
/**
|
||||
* DTO pubblici: forma delle risposte JSON di apps/api consumate dai frontend.
|
||||
* Date in ISO 8601 (string), importi in centesimi di euro (integer), id uuid.
|
||||
* Non contengono mai campi interni (hash, note admin, path su disco, payload grezzi).
|
||||
*/
|
||||
import type {
|
||||
BlogPostStatus,
|
||||
ConsentType,
|
||||
ContactRequestStatus,
|
||||
DocumentVisibility,
|
||||
InstallerStatus,
|
||||
QuoteRequestStatus,
|
||||
QuoteStatus,
|
||||
ShowroomBookingStatus,
|
||||
TicketPriority,
|
||||
TicketStatus,
|
||||
} from "./domain.js"
|
||||
import type { UserRole } from "./user-role.js"
|
||||
|
||||
export type IsoDateTime = string
|
||||
|
||||
export interface UserDto {
|
||||
id: string
|
||||
email: string
|
||||
role: UserRole
|
||||
firstName: string
|
||||
lastName: string
|
||||
companyName: string | null
|
||||
vatNumber: string | null
|
||||
emailVerified: boolean
|
||||
createdAt: IsoDateTime
|
||||
}
|
||||
|
||||
/** Utente visto dall'admin (GET /admin/users): stato dell'account e collegamenti. */
|
||||
export interface UserAdminDto extends UserDto {
|
||||
disabledAt: IsoDateTime | null
|
||||
/** True se l'utente ha un customer Medusa collegato (l'id non viene esposto). */
|
||||
hasShopAccount: boolean
|
||||
/** Stato del profilo installatore, se esiste. */
|
||||
installerStatus: InstallerStatus | null
|
||||
updatedAt: IsoDateTime
|
||||
}
|
||||
|
||||
/** Contatori della dashboard admin (GET /admin/summary). */
|
||||
export interface AdminSummaryDto {
|
||||
/** Richieste di preventivo con stato `new`. */
|
||||
newQuoteRequests: number
|
||||
/** Ticket `open` (in attesa dell'assistenza). */
|
||||
openTickets: number
|
||||
/** Ticket non chiusi (qualunque stato tranne `closed`). */
|
||||
activeTickets: number
|
||||
/** Profili installatore `pending`. */
|
||||
pendingInstallers: number
|
||||
/** Prenotazioni showroom attive (`requested`/`confirmed`) da oggi in poi. */
|
||||
upcomingShowroomBookings: number
|
||||
/** Richieste dal form contatti con stato `new`. */
|
||||
newContactRequests: number
|
||||
/** Preventivi inviati in attesa di risposta del cliente. */
|
||||
sentQuotes: number
|
||||
}
|
||||
|
||||
export interface GeoPoint {
|
||||
lat: number
|
||||
lng: number
|
||||
}
|
||||
|
||||
export interface InstallerProfileDto {
|
||||
userId: string
|
||||
businessName: string
|
||||
vatNumber: string | null
|
||||
phone: string | null
|
||||
address: string | null
|
||||
location: GeoPoint | null
|
||||
serviceRadiusKm: number
|
||||
certifications: string[]
|
||||
status: InstallerStatus
|
||||
approvedAt: IsoDateTime | null
|
||||
}
|
||||
|
||||
/** Profilo installatore visto dall'admin: dati dell'account e note interne. */
|
||||
export interface InstallerAdminDto extends InstallerProfileDto {
|
||||
email: string
|
||||
firstName: string
|
||||
lastName: string
|
||||
/** Note interne dell'admin (mai esposte all'installatore). */
|
||||
notes: string | null
|
||||
disabled: boolean
|
||||
createdAt: IsoDateTime
|
||||
}
|
||||
|
||||
/** Installatore che copre un punto, con la distanza dalla sua sede. */
|
||||
export interface NearbyInstallerDto {
|
||||
userId: string
|
||||
businessName: string
|
||||
email: string
|
||||
phone: string | null
|
||||
address: string | null
|
||||
location: GeoPoint
|
||||
serviceRadiusKm: number
|
||||
certifications: string[]
|
||||
distanceKm: number
|
||||
}
|
||||
|
||||
export interface QuoteRequestDto {
|
||||
id: string
|
||||
email: string
|
||||
name: string
|
||||
phone: string | null
|
||||
province: string | null
|
||||
configuratorInput: unknown
|
||||
configuratorOutput: unknown
|
||||
status: QuoteRequestStatus
|
||||
createdAt: IsoDateTime
|
||||
}
|
||||
|
||||
/** Richiesta di preventivo vista dall'admin (con l'eventuale account collegato). */
|
||||
export interface QuoteRequestAdminDto extends QuoteRequestDto {
|
||||
userId: string | null
|
||||
updatedAt: IsoDateTime
|
||||
}
|
||||
|
||||
export interface QuoteDto {
|
||||
id: string
|
||||
quoteRequestId: string | null
|
||||
number: string
|
||||
title: string
|
||||
status: QuoteStatus
|
||||
amountCents: number
|
||||
/** True se c'è un PDF allegato: il cliente lo scarica da GET /me/quotes/:id/pdf. */
|
||||
hasPdf: boolean
|
||||
/** Data (YYYY-MM-DD) oltre la quale il preventivo scade; null = senza scadenza. */
|
||||
validUntil: string | null
|
||||
sentAt: IsoDateTime | null
|
||||
createdAt: IsoDateTime
|
||||
}
|
||||
|
||||
/** Preventivo visto dall'admin: cliente destinatario e documento PDF. */
|
||||
export interface QuoteAdminDto extends QuoteDto {
|
||||
userId: string
|
||||
customer: { email: string; firstName: string; lastName: string; companyName: string | null }
|
||||
pdfDocumentId: string | null
|
||||
}
|
||||
|
||||
export interface TicketDto {
|
||||
id: string
|
||||
subject: string
|
||||
status: TicketStatus
|
||||
priority: TicketPriority
|
||||
createdAt: IsoDateTime
|
||||
updatedAt: IsoDateTime
|
||||
}
|
||||
|
||||
export interface TicketDetailDto extends TicketDto {
|
||||
messages: TicketMessageDto[]
|
||||
}
|
||||
|
||||
/** Ticket visto dall'admin: autore della richiesta. */
|
||||
export interface TicketAdminDto extends TicketDto {
|
||||
userId: string
|
||||
customer: { email: string; firstName: string; lastName: string }
|
||||
}
|
||||
|
||||
export interface TicketMessageDto {
|
||||
id: string
|
||||
ticketId: string
|
||||
authorId: string
|
||||
/** true se scritto da un admin (risposta dell'assistenza). */
|
||||
fromStaff: boolean
|
||||
body: string
|
||||
createdAt: IsoDateTime
|
||||
}
|
||||
|
||||
export interface BlogCategoryDto {
|
||||
id: string
|
||||
slug: string
|
||||
name: string
|
||||
description: string | null
|
||||
}
|
||||
|
||||
export interface BlogPostSummaryDto {
|
||||
id: string
|
||||
slug: string
|
||||
title: string
|
||||
excerpt: string | null
|
||||
coverUrl: string | null
|
||||
category: BlogCategoryDto | null
|
||||
publishedAt: IsoDateTime | null
|
||||
}
|
||||
|
||||
export interface BlogPostDetailDto extends BlogPostSummaryDto {
|
||||
contentMd: string
|
||||
/** HTML renderizzato e sanitizzato lato API. */
|
||||
contentHtml: string
|
||||
seoTitle: string | null
|
||||
seoDescription: string | null
|
||||
updatedAt: IsoDateTime
|
||||
}
|
||||
|
||||
/** Usato dall'admin: include lo stato editoriale. */
|
||||
export interface BlogPostAdminDto extends BlogPostDetailDto {
|
||||
status: BlogPostStatus
|
||||
}
|
||||
|
||||
export interface DocumentDto {
|
||||
id: string
|
||||
title: string
|
||||
category: string | null
|
||||
mimeType: string
|
||||
sizeBytes: number
|
||||
visibility: DocumentVisibility
|
||||
productRef: string | null
|
||||
createdAt: IsoDateTime
|
||||
}
|
||||
|
||||
export interface ShowroomBookingDto {
|
||||
id: string
|
||||
name: string
|
||||
email: string
|
||||
phone: string | null
|
||||
requestedAt: IsoDateTime
|
||||
people: number
|
||||
notes: string | null
|
||||
status: ShowroomBookingStatus
|
||||
createdAt: IsoDateTime
|
||||
}
|
||||
|
||||
/** Prenotazione showroom vista dall'admin: con l'utente collegato (se prenotata da autenticato). */
|
||||
export interface ShowroomBookingAdminDto extends ShowroomBookingDto {
|
||||
userId: string | null
|
||||
updatedAt: IsoDateTime
|
||||
}
|
||||
|
||||
/** Slot di visita allo showroom (GET /showroom/slots). */
|
||||
export interface ShowroomSlotDto {
|
||||
start: IsoDateTime
|
||||
end: IsoDateTime
|
||||
/** Posti (persone) ancora prenotabili; 0 = completo. */
|
||||
available: number
|
||||
}
|
||||
|
||||
export interface ShowroomSlotsDto {
|
||||
timeZone: string
|
||||
slotMinutes: number
|
||||
/** Persone massime per singola prenotazione. */
|
||||
maxPeoplePerBooking: number
|
||||
/** Solo i giorni di apertura nel periodo richiesto; date locali YYYY-MM-DD. */
|
||||
days: { date: string; slots: ShowroomSlotDto[] }[]
|
||||
}
|
||||
|
||||
/** Richiesta dal form contatti (solo admin). */
|
||||
export interface ContactRequestDto {
|
||||
id: string
|
||||
userId: string | null
|
||||
name: string
|
||||
email: string
|
||||
phone: string | null
|
||||
subject: string | null
|
||||
message: string
|
||||
status: ContactRequestStatus
|
||||
createdAt: IsoDateTime
|
||||
updatedAt: IsoDateTime
|
||||
}
|
||||
|
||||
/** Token anti-spam dei form pubblici (GET /forms/token). */
|
||||
export interface FormTokenDto {
|
||||
token: string
|
||||
/** Secondi minimi tra emissione del token e invio del form. */
|
||||
minSeconds: number
|
||||
expiresAt: IsoDateTime
|
||||
}
|
||||
|
||||
export interface ConsentDto {
|
||||
id: string
|
||||
type: ConsentType
|
||||
version: string
|
||||
/** Form di provenienza (register, contact, showroom, configurator, account). */
|
||||
source: string | null
|
||||
acceptedAt: IsoDateTime
|
||||
revokedAt: IsoDateTime | null
|
||||
}
|
||||
|
||||
/** Ordine del cliente (da Medusa). Importi in centesimi IVA inclusa. */
|
||||
export interface MyOrderDto {
|
||||
/** Numero ordine leggibile (display_id Medusa). */
|
||||
number: number
|
||||
status: string
|
||||
paymentStatus: string
|
||||
fulfillmentStatus: string
|
||||
createdAt: IsoDateTime
|
||||
totals: { items: number; shipping: number; tax: number; total: number }
|
||||
items: { title: string; sku: string | null; thumbnail: string | null; quantity: number; unitPrice: number; total: number }[]
|
||||
}
|
||||
|
||||
/** Dettaglio di un ordine del cliente: totali con sconto, spedizione e tracking. */
|
||||
export interface MyOrderDetailDto extends Omit<MyOrderDto, "totals"> {
|
||||
totals: { items: number; shipping: number; discount: number; tax: number; total: number }
|
||||
/** Indirizzo di spedizione già composto in righe (nome, azienda, via, CAP città (provincia)); vuoto se assente. */
|
||||
shippingAddress: string[]
|
||||
/** Metodi di spedizione scelti (es. "Spedizione standard"). */
|
||||
shippingMethods: string[]
|
||||
/** Codici di tracking delle spedizioni, se presenti. */
|
||||
tracking: { number: string; url: string | null }[]
|
||||
}
|
||||
|
||||
/** Risposta paginata standard. */
|
||||
export interface Paginated<T> {
|
||||
items: T[]
|
||||
total: number
|
||||
limit: number
|
||||
offset: number
|
||||
}
|
||||
|
||||
// --- Shop (proxy Medusa, prompt 13) ---
|
||||
// Importi in centesimi di euro, IVA inclusa. Prodotti e categorie identificati dall'handle;
|
||||
// gli unici id Medusa esposti sono quelli che il frontend deve rimandare (variante, riga, opzione di spedizione).
|
||||
|
||||
/** "installer": prezzo dal listino riservato installatori; "standard": prezzo di listino pubblico. */
|
||||
export type ShopPriceType = "standard" | "installer"
|
||||
|
||||
export interface ShopCategoryDto {
|
||||
handle: string
|
||||
name: string
|
||||
description: string | null
|
||||
parentHandle: string | null
|
||||
}
|
||||
|
||||
export interface ShopProductSummaryDto {
|
||||
handle: string
|
||||
title: string
|
||||
subtitle: string | null
|
||||
thumbnail: string | null
|
||||
/** Prezzo più basso tra le varianti (null se nessuna variante ha un prezzo). */
|
||||
fromPrice: number | null
|
||||
/**
|
||||
* Prezzo di listino pubblico della stessa variante di `fromPrice`: con `priceType: "installer"` è il prezzo
|
||||
* da confrontare con quello riservato (listino installatori). Null se non c'è prezzo.
|
||||
*/
|
||||
fromOriginalPrice: number | null
|
||||
priceType: ShopPriceType
|
||||
categories: { handle: string; name: string }[]
|
||||
}
|
||||
|
||||
export interface ShopVariantDto {
|
||||
/** Da inviare a POST /shop/cart/items. */
|
||||
id: string
|
||||
title: string
|
||||
sku: string | null
|
||||
price: number | null
|
||||
/** Prezzo di listino pubblico (diverso da price se si applica un listino riservato). */
|
||||
originalPrice: number | null
|
||||
inStock: boolean
|
||||
options: { name: string; value: string }[]
|
||||
}
|
||||
|
||||
export interface ShopProductDetailDto extends ShopProductSummaryDto {
|
||||
description: string | null
|
||||
images: string[]
|
||||
variants: ShopVariantDto[]
|
||||
}
|
||||
|
||||
export interface ShopAddressDto {
|
||||
firstName: string
|
||||
lastName: string
|
||||
company: string | null
|
||||
address1: string
|
||||
address2: string | null
|
||||
city: string
|
||||
postalCode: string
|
||||
province: string | null
|
||||
countryCode: string
|
||||
phone: string | null
|
||||
}
|
||||
|
||||
/**
|
||||
* Dati per la fattura a un'azienda, chiesti al checkout. Salvati nei metadata del carrello Medusa
|
||||
* (`metadata.invoice`), che passano all'ordine.
|
||||
*/
|
||||
export interface ShopInvoiceDto {
|
||||
companyName: string
|
||||
/** Partita IVA italiana (11 cifre). */
|
||||
vatNumber: string
|
||||
/** Codice fiscale dell'azienda, se diverso dalla partita IVA. */
|
||||
fiscalCode: string | null
|
||||
/** Codice destinatario SDI (7 caratteri). */
|
||||
sdiCode: string | null
|
||||
pec: string | null
|
||||
}
|
||||
|
||||
export interface ShopCartItemDto {
|
||||
/** Id della riga: per PATCH/DELETE /shop/cart/items/:id. */
|
||||
id: string
|
||||
variantId: string
|
||||
productHandle: string | null
|
||||
productTitle: string
|
||||
variantTitle: string | null
|
||||
sku: string | null
|
||||
thumbnail: string | null
|
||||
quantity: number
|
||||
unitPrice: number
|
||||
total: number
|
||||
}
|
||||
|
||||
export interface ShopCartDto {
|
||||
email: string | null
|
||||
items: ShopCartItemDto[]
|
||||
itemCount: number
|
||||
shippingAddress: ShopAddressDto | null
|
||||
billingAddress: ShopAddressDto | null
|
||||
/** Dati per la fattura aziendale (null = nessuna fattura richiesta). */
|
||||
invoice: ShopInvoiceDto | null
|
||||
shippingMethod: { optionId: string; name: string; amount: number } | null
|
||||
totals: {
|
||||
items: number
|
||||
shipping: number
|
||||
discount: number
|
||||
/** IVA compresa nel totale (scorporata). */
|
||||
tax: number
|
||||
total: number
|
||||
}
|
||||
/** True se il carrello ha email, indirizzo, spedizione e almeno una riga. */
|
||||
readyForPayment: boolean
|
||||
}
|
||||
|
||||
export interface ShopShippingOptionDto {
|
||||
/** Da inviare a POST /shop/cart/shipping. */
|
||||
id: string
|
||||
name: string
|
||||
amount: number
|
||||
}
|
||||
|
||||
export interface ShopPaymentDto {
|
||||
provider: "stripe"
|
||||
/** client_secret del PaymentIntent Stripe, per confirmPayment con Stripe.js. */
|
||||
clientSecret: string
|
||||
amount: number
|
||||
}
|
||||
|
||||
export interface ShopOrderDto {
|
||||
/** Numero ordine leggibile (display_id Medusa). */
|
||||
number: number
|
||||
email: string
|
||||
createdAt: IsoDateTime
|
||||
items: { productTitle: string; variantTitle: string | null; quantity: number; unitPrice: number; total: number }[]
|
||||
totals: { items: number; shipping: number; tax: number; total: number }
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
export type { UserRole } from "./user-role.js"
|
||||
export { USER_ROLES } from "./user-role.js"
|
||||
export * from "./domain.js"
|
||||
export type * from "./dto.js"
|
||||
export * from "./configurator.js"
|
||||
export * from "./validation.js"
|
||||
@@ -0,0 +1,4 @@
|
||||
/** Ruoli applicativi di un utente (colonna `users.role`, enum Postgres `user_role`). */
|
||||
export const USER_ROLES = ["customer", "company", "installer", "admin"] as const
|
||||
|
||||
export type UserRole = (typeof USER_ROLES)[number]
|
||||
@@ -0,0 +1,34 @@
|
||||
// Validazioni condivise tra API e frontend (funzioni pure, nessuna dipendenza).
|
||||
|
||||
/** Normalizza una partita IVA italiana: senza spazi e senza prefisso "IT". */
|
||||
export function normalizeVatNumber(value: string): string {
|
||||
return value.replace(/\s+/g, "").toUpperCase().replace(/^IT/, "")
|
||||
}
|
||||
|
||||
/** Partita IVA italiana valida: 11 cifre con cifra di controllo corretta (algoritmo di Luhn). */
|
||||
export function isValidItalianVat(value: string): boolean {
|
||||
const vat = normalizeVatNumber(value)
|
||||
if (!/^\d{11}$/.test(vat)) return false
|
||||
let sum = 0
|
||||
for (let i = 0; i < 10; i++) {
|
||||
let d = Number(vat[i])
|
||||
if (i % 2 === 1) {
|
||||
d *= 2
|
||||
if (d > 9) d -= 9
|
||||
}
|
||||
sum += d
|
||||
}
|
||||
return (10 - (sum % 10)) % 10 === Number(vat[10])
|
||||
}
|
||||
|
||||
/** Codice fiscale: 16 caratteri alfanumerici (persona) o 11 cifre (azienda). Controllo solo di formato. */
|
||||
export const FISCAL_CODE_PATTERN = /^([A-Z0-9]{16}|\d{11})$/
|
||||
|
||||
/** Codice destinatario SDI: 7 caratteri alfanumerici (es. "0000000" per chi non ne ha uno). */
|
||||
export const SDI_CODE_PATTERN = /^[A-Z0-9]{7}$/
|
||||
|
||||
/** Riquadro che contiene l'Italia (isole minori comprese): le coordinate fuori sono un errore di input. */
|
||||
export const ITALY_BOUNDS = { minLat: 35.4, maxLat: 47.1, minLng: 6.6, maxLng: 18.6 } as const
|
||||
|
||||
export const isInItaly = (p: { lat: number; lng: number }): boolean =>
|
||||
p.lat >= ITALY_BOUNDS.minLat && p.lat <= ITALY_BOUNDS.maxLat && p.lng >= ITALY_BOUNDS.minLng && p.lng <= ITALY_BOUNDS.maxLng
|
||||
@@ -0,0 +1,8 @@
|
||||
{
|
||||
"extends": "../../tsconfig.base.json",
|
||||
"compilerOptions": {
|
||||
"rootDir": "src",
|
||||
"outDir": "dist"
|
||||
},
|
||||
"include": ["src"]
|
||||
}
|
||||
Reference in New Issue
Block a user