La conținut
Type — API pentru site
Secțiuni

Instalare și pornire rapidă

@type/site-client este clientul de server pentru API-ul site-ului: metode tipizate, erori sub formă de clase și verificarea semnăturii webhook-ului. Nu are dependențe; are nevoie de fetch și Web Crypto, de aceea funcționează în Node.js 20+ și în runtime-ul Edge. Nu este legat de un framework — mai jos sunt exemple pentru Next.js (App Router).

Instalare

Pachetul nu este încă publicat în registrul public npm. Arhiva type-site-client-0.5.0.tgz o oferă proprietarul contului CRM; puneți-o în depozitul site-ului (de exemplu, în vendor/) și instalați:

bash
pnpm add ./vendor/type-site-client-0.5.0.tgz
# sau: npm install ./vendor/type-site-client-0.5.0.tgz

Versiunea curentă — 0.5.0, modificările — în istoricul versiunilor.

Pachetul funcționează doar pe server. La import într-un bundle de browser aruncă imediat o eroare — astfel cheia nu ajunge din greșeală pe pagină. În Next.js adăugați și import 'server-only' în modulul care creează clientul.

Variabile de mediu

Doar de server — fără prefixul NEXT_PUBLIC_:

bash
TYPE_API_URL=https://crm.example.com      # adresa CRM-ului dvs., fără cale
TYPE_API_KEY=tk_<key_id>_<secret>          # cheia cu dreptul „Chirii: site”
TYPE_WEBHOOK_SECRET=<secretul webhook-ului> # Setări → Chirii → Site

Fișierele .env* cu aceste valori nu trebuie să ajungă în depozit.

Prima cerere

ts
// lib/crm.ts
import 'server-only';
import { createSiteClient } from '@type/site-client';

export const crm = createSiteClient({
  baseUrl: process.env.TYPE_API_URL!,
  apiKey: process.env.TYPE_API_KEY!,
});

// Verificarea conexiunii: compania, moneda, limbile, ora check-in și check-out.
const info = await crm.ping();

baseUrl se acceptă doar cu https:// (în afară de localhost), altfel cheia ar pleca în text clar.

Metode

MetodăRutăCe întoarce
ping()GET /pingcompania, moneda, limbile, ora check-in și check-out
groups()GET /groupsraioanele cu numărul de obiecte
units({ lang })GET /unitstoate obiectele (în afară de cele arhivate)
unit(id, { lang })GET /units/{id}un obiect
occupancy({ date_from, date_to, unit_ids })GET /occupancyintervalele ocupate; date_to exclusiv, perioada de până la 400 de zile
quote({ unit_id, check_in, check_out, guests })GET /quotecalculul pe nopți; la date ocupate — available: false și violations
prices({ check_in, check_out, guests, group_id })GET /pricesprețul și disponibilitatea tuturor obiectelor pe date
createBookingRequest(body, { visitorIp })POST /booking-requestscererea de rezervare
legacyUrls()GET /legacy-urlsadresele vechi pentru redirecționări
photoUrl(url)—adresa absolută a fotografiei

Separat, fără client, se exportă funcția verifyWebhook(body, headers, secret) — verificarea semnăturii webhook-ului.

Căile rutelor pornesc de la /api/modules/rental/site. Fiecare metodă primește ca ultim argument { signal, init }: signal — pentru anulare, init — câmpuri suplimentare fetch (antetul Authorization nu poate fi suprascris prin ele).

Implicit clientul setează cache: 'no-store'. Puneți rezultatele în cache singuri — de exemplu, unstable_cache cu etichete — și goliți-le prin webhook.

Catalog: componentă de server

tsx
// app/[lang]/apartamente/page.tsx
import { unstable_cache } from 'next/cache';
import { crm } from '@/lib/crm';
import type { Lang } from '@type/site-client';

const getUnits = unstable_cache((lang: Lang) => crm.units({ lang }), ['rental-units'], {
  tags: ['rental:units'],
  revalidate: 3600,
});

export default async function Catalog({ params }: { params: Promise<{ lang: Lang }> }) {
  const { lang } = await params;
  const { units } = await getUnits(lang);
  return (
    <ul>
      {units.map((u) => (
        <li key={u.id}>
          <a href={`/${lang}/apartament/${u.slug}`}>
            {u.photos[0] && <img src={crm.photoUrl(u.photos[0].url)} alt={u.name} />}
            <h2>{u.name}</h2>
            <p>{u.price} {u.currency}</p>
          </a>
        </li>
      ))}
    </ul>
  );
}

Linkurile la fotografii sunt permanente și se deschid fără cheie: CRM-ul răspunde cu redirecționare 302 spre fișierul din stocare. Pentru next/image permiteți în images.remotePatterns host-ul CRM-ului și host-ul stocării sau folosiți unoptimized.

Cererea de rezervare: route handler

ts
// app/api/booking/route.ts
import { RentalUnavailableError, SiteNetworkError, TooManyAttemptsError } from '@type/site-client';
import { crm } from '@/lib/crm';

export async function POST(req: Request) {
  const f = await req.json();
  // IP-ul vizitatorului — din antetul proxy-ului dvs.; după el CRM-ul numără limita cererilor.
  const visitorIp = req.headers.get('x-forwarded-for')?.split(',')[0]?.trim();
  try {
    const res = await crm.createBookingRequest(
      {
        request_id: f.request_id, // UUID-ul formularului: repetarea cu același id nu creează a doua cerere
        unit_id: f.unit_id,
        check_in: f.check_in,
        check_out: f.check_out,
        guests: Number(f.guests ?? 1),
        name: f.name,
        phone: f.phone || null,
        email: f.email || null,
        message: f.message || null,
        lang: f.lang,
        page_url: f.page_url ?? null,
        consent: true,
        hp: f.hp ?? '', // câmp-capcană ascuns, trebuie să fie gol
        elapsed_ms: Number(f.elapsed_ms ?? 0), // cât timp a fost deschis formularul
      },
      visitorIp ? { visitorIp } : {},
    );
    return Response.json(res, { status: 201 });
  } catch (e) {
    if (e instanceof RentalUnavailableError) return Response.json({ violations: e.violations }, { status: 422 });
    if (e instanceof TooManyAttemptsError) {
      return Response.json({ error: 'too_many_attempts' }, { status: 429, headers: { 'Retry-After': String(e.retryAfter ?? 60) } });
    }
    if (e instanceof SiteNetworkError) return Response.json({ error: 'unavailable' }, { status: 503 });
    throw e;
  }
}

Cererea creează în CRM o tranzacție la etapa „Cerere” cu calculul prețului; clientul se caută după telefon, apoi după e-mail. Dacă CRM-ul a fost indisponibil, repetați trimiterea cu același request_id — a doua tranzacție nu va apărea.

Antispam: câmpul hp trebuie să fie gol, elapsed_ms — cel puțin 3000. Dacă verificarea nu trece, răspunsul este la fel ca la succes, dar cererea nu se creează.

Erori

Toate erorile moștenesc SiteApiError cu câmpurile status, code și body.

ClasăCând
ApiKeyInvalidError401 api_key_invalid — cheia lipsește, e greșită, revocată, expirată sau IP-ul nu e permis. Motivul se vede doar în CRM, în jurnalul cheii
ScopeMissingError403 scope_missing — cheia nu are dreptul; scope — care (rental:site)
ScopeUnavailableError403 scope_unavailable — dreptul există, dar modulul de închiriere este dezactivat
TooManyAttemptsError429 too_many_attempts; retryAfter — secundele din Retry-After sau null
RentalUnavailableError422 rental_unavailable la cerere; violations — de ce
SiteApiErrorrestul: validation_error, not_found, bad_response…
SiteNetworkErrorCRM-ul nu a răspuns: rețea sau timeout (5 s pentru citire, 10 s pentru cerere)

Erorile cheii (401, 403) sunt o problemă de configurare a site-ului, nu a vizitatorului: înregistrați code în jurnal și arătați vizitatorului un mesaj neutru „serviciul este temporar indisponibil”. Cheia în sine nu apare în mesajele erorilor.