[{"data":1,"prerenderedAt":330},["ShallowReactive",2],{"work-en-ospitio":3},{"id":4,"title":5,"body":6,"category":307,"description":93,"extension":308,"featured":309,"links":310,"meta":311,"navigation":309,"order":312,"path":313,"period":314,"role":315,"seo":316,"slug":317,"stack":318,"stem":327,"subtitle":328,"summary":310,"__hash__":329},"projects_en\u002Fprojects\u002Fen\u002Fospitio.md","Ospitio",{"type":7,"value":8,"toc":298},"minimark",[9,14,18,44,47,51,58,62,80,86,96,111,115,121,161,179,200,211,224,234,238,280,284,295],[10,11,13],"h2",{"id":12},"the-problem","The problem",[15,16,17],"p",{},"Every Italian lodging business has to handle two mandatory per-guest workflows:",[19,20,21,38],"ol",{},[22,23,24,28,29,32,33,37],"li",{},[25,26,27],"strong",{},"Alloggiati Web"," — run by the Italian State Police. Guest records must be filed within 24 hours of check-in. The only official channel is a SOAP API that accepts TXT files with ",[25,30,31],{},"fixed-width 168-character records",". 15 columns, each with its own padding (50 for surname, 30 for first name, 2 for the Italian province code, 9 for the ISTAT code of the birth city). One character off position = record rejected with a cryptic ",[34,35,36],"code",{},"SCHEDINA_CAMPO_NON_CORRETTO"," error.",[22,39,40,43],{},[25,41,42],{},"Paytourist"," — municipal platform for tourist-tax management. REST API, one instance per province (Bari, Rome, Florence — each with its own endpoint), structure token renewable every 36 months.",[15,45,46],{},"A typical owner re-enters the same guest data into three systems (their own bookings tool + Alloggiati + Paytourist), fumbles one record in twenty, loses 15-30 minutes per reservation, and carries the compliance-deadline anxiety on top.",[10,48,50],{"id":49},"why-i-built-it","Why I built it",[15,52,53,54,57],{},"I run a small B&B. I lived the problem for months before I decided automation was the only sustainable path. ",[25,55,56],{},"Every product decision was made against a concrete lived use case"," — not a persona invented in a workshop.",[10,59,61],{"id":60},"architecture","Architecture",[15,63,64,67,68,71,72,75,76,79],{},[25,65,66],{},"Frontend"," — Nuxt 3 SPA + Nuxt UI + TypeScript, SSG build on static hosting. No Pinia stores: just Nuxt's native ",[34,69,70],{},"useState()"," plus 11 composables that wrap the API calls. A single ",[34,73,74],{},"useApi()"," layer around ",[34,77,78],{},"$fetch"," handles bearer-token injection and request-context headers.",[15,81,82,85],{},[25,83,84],{},"Backend"," — NestJS + Prisma + PostgreSQL, containerised deploy. 14 domain modules:",[87,88,94],"pre",{"className":89,"code":91,"language":92,"meta":93},[90],"language-text","auth              ← JWT + registration \u002F login \u002F password reset\nusers             ← user management\npermissions       ← granular RBAC\nproperties        ← accommodations + credentials for integrations\nrooms             ← rooms + mapping to the tax platform\nguests            ← guest master data\nguest-documents   ← identity documents per guest\nstays             ← reservations\nstay-guests       ← stay↔guest pivot with role\nexports           ← 168-char TXT generator for the public-security portal\nalloggiati-web    ← SOAP client for the public-security portal\npaytourist        ← REST client for the tourist-tax platform\ncheckin-intake    ← webhook ingress for external self-check-in modules\naudit             ← log of entity changes\n","text","",[34,95,91],{"__ignoreMap":93},[15,97,98,99,102,103,106,107,110],{},"Three cross-cutting guards: a global ",[34,100,101],{},"JwtAuthGuard"," for token validation, a ",[34,104,105],{},"PermissionsGuard"," for granular permissions, and an ",[34,108,109],{},"ImpersonationInterceptor"," that lets an admin — only if authorised — operate in the context of another user through a dedicated channel.",[10,112,114],{"id":113},"key-technical-decisions","Key technical decisions",[15,116,117,120],{},[25,118,119],{},"1. SOAP where you never want to see it."," The State Police portal speaks SOAP only. I wrote a minimal SOAP client by hand, no heavy library. It handles:",[122,123,124,141,144],"ul",{},[22,125,126,129,130,133,134,133,137,140],{},[34,127,128],{},"GenerateToken"," with the structure's ",[34,131,132],{},"user"," \u002F ",[34,135,136],{},"password",[34,138,139],{},"wsKey"," credentials",[22,142,143],{},"in-memory token caching (~1h TTL) with automatic refresh",[22,145,146,147,150,151,154,155,150,158,160],{},"two method pairs depending on the user type: ",[34,148,149],{},"Send()","\u002F",[34,152,153],{},"Test()"," for standard users, ",[34,156,157],{},"GestioneAppartamenti_Send()",[34,159,153],{}," for property managers with multiple apartments",[15,162,163,166,167,170,171,174,175,178],{},[25,164,165],{},"2. The fixed-width formatter is a first-class citizen."," ",[34,168,169],{},"alloggiati.formatter.ts"," maps ",[34,172,173],{},"Guest + GuestDocument + Stay + StayGuestRole"," to a 168-character line with column-specific padding. Kept separate from validator and exporter (single responsibility). The ",[34,176,177],{},"StayGuestRole"," (SINGLE \u002F FAMILY_HEAD \u002F FAMILY_MEMBER \u002F GROUP_HEAD \u002F GROUP_MEMBER) determines the \"Tipo Alloggiato\" code (16-20) — a non-trivial rule because it depends on the composition of the party.",[15,180,181,184,185,188,189,188,192,195,196,199],{},[25,182,183],{},"3. Lightweight multi-tenancy."," One backend instance, N users, each with their own accommodations. Every operational table (",[34,186,187],{},"Guest",", ",[34,190,191],{},"Stay",[34,193,194],{},"Export",", …) carries an ",[34,197,198],{},"ownerId"," FK; all services filter by owner. Simpler than schema-per-tenant, safer than trusting the app layer blindly.",[15,201,202,205,206,210],{},[25,203,204],{},"4. Impersonation instead of shared passwords."," To assist a user, I can step into their account through a dedicated flow, gated by a specific permission. No more ",[207,208,209],"em",{},"\"send me your password for a sec\"",". Every impersonated action is logged with both the operator and the target user.",[15,212,213,216,217,220,221,223],{},[25,214,215],{},"5. Test mode as a gesture of respect."," Before sending records to the State Police, the user can click ",[25,218,219],{},"Test",": the backend calls the SOAP ",[34,222,153],{}," method which validates without committing. Errors come back per-record with the original code and a human-readable translation. No surprises after Send.",[15,225,226,229,230,233],{},[25,227,228],{},"6. Kiosk-to-stay webhook."," An external self-check-in module can create ",[34,231,232],{},"Guest + GuestDocument + Stay"," through an authenticated webhook endpoint. When a guest fills the form at the front door, the data reaches the backend already structured — ready to be filed with the public-security portal.",[10,235,237],{"id":236},"numbers","Numbers",[122,239,240,247,256,262,268,274],{},[22,241,242,243,246],{},"~",[25,244,245],{},"12,000 LOC"," total (6.7k frontend · 5.3k backend)",[22,248,249,252,253],{},[25,250,251],{},"14 Prisma models"," · ",[25,254,255],{},"14 NestJS modules",[22,257,258,261],{},[25,259,260],{},"15 fixed-width columns"," in the Alloggiati format, with lookups against ISTAT tables for Italian cities and countries",[22,263,264,267],{},[25,265,266],{},"2 regulatory integrations"," with opposite semantics (SOAP XML fixed-width vs REST JSON)",[22,269,270,273],{},[25,271,272],{},"5 granular permissions"," wired to specific endpoints",[22,275,276,279],{},[25,277,278],{},"1 kiosk webhook"," closing the loop from the front desk to the backend in a single hop",[10,281,283],{"id":282},"takeaways","Takeaways",[15,285,286,287,290,291,294],{},"Working on an Italian public-bureaucracy domain teaches you one thing: the ",[207,288,289],{},"\"decent API\""," assumption doesn't hold. Abstraction is only worth it when we write it. End-user value scales ",[25,292,293],{},"multiplicatively"," — not additively — with the amount of regulatory complexity the software manages to hide.",[15,296,297],{},"Building software for a problem you have yourself carries a cheat code: you already know exactly where the user gives up. No user research needed — you just can't forget that moment.",{"title":93,"searchDepth":299,"depth":299,"links":300},2,[301,302,303,304,305,306],{"id":12,"depth":299,"text":13},{"id":49,"depth":299,"text":50},{"id":60,"depth":299,"text":61},{"id":113,"depth":299,"text":114},{"id":236,"depth":299,"text":237},{"id":282,"depth":299,"text":283},"work","md",true,null,{},1,"\u002Fprojects\u002Fen\u002Fospitio","2026 → ongoing","Founder · full-stack",{"title":5,"description":93},"ospitio",[319,320,321,322,323,324,325,326],"Nuxt 3","Nuxt UI","NestJS","Prisma","PostgreSQL","TypeScript","SOAP","REST","projects\u002Fen\u002Fospitio","A management SaaS for Italian accommodations that automates the two mandatory regulatory workflows — Alloggiati Web and Paytourist — in a single platform.","9cWuYuIgUHfbR_9TQEBLiua4L-HRWJA1COa7T_1d4t4",1781346783479]