Integracja standardów ACORD i Insurance API
Ekosystem ubezpieczeniowy jest fragmentaryczny ze względu na swój charakter: towarzystwa ubezpieczeniowe, reasekuratorzy, brokerzy, agencje, banki (bancassurance), porównywarki internetowe, dostawcy usług (warsztaty, szpitale, prawnicy) – wszystkie te podmioty muszą wymieniać dane skutecznie, dokładnie i w sposób kontrolowany. Bez wspólnych standardów, każda integracja staje się projektem niestandardowym o wysokich kosztach i ryzyku.
POGOD (Stowarzyszenie na rzecz Badań i Rozwoju Operacji Spółdzielczych) oraz globalna organizacja wyznaczająca standardy danych dla branży ubezpieczeniowej od 1970 r. Jej standardy — komunikaty XML i JSON do zarządzania polisami, roszczeniami, regulacje dotyczące reasekuracji i rachunkowości – przejęto z art organizacji w 100 krajach. W 2025 roku ACORD uruchomił m.in Standardy danych GRLC generacji 2.0, kompleksowy przegląd standardów reasekuracji i dużych ryzyk handlowych natywna obsługa JSON i zorientowanie na API.
Ten przewodnik tłumaczy standardy ACORD na konkretny kod: jak ustrukturyzować komunikaty Zgodny ze standardami XML/JSON, jak zbudować warstwę mapującą pomiędzy autorskimi systemami i ACORD oraz jak udostępniać interfejsy API RESTful, które są zgodne z najlepszymi praktykami branżowymi.
Czego się nauczysz
- Przegląd standardów ACORD: XML i JSON dla różnych branż
- Struktura komunikatów ACORD dotyczących polis i roszczeń
- Mapowanie pomiędzy zastrzeżonymi modelami danych a ACORD
- Implementacja bramy API zgodnej z ACORD w Python/FastAPI
- Walidacja wiadomości za pomocą schematu XML (XSD) i schematu JSON
- Wzorce integracji: punkt-punkt, ESB, sterowane zdarzeniami
- Certyfikacja ACORD: wymagania i proces testowania
Przegląd standardów ACORD
ACORD publikuje różne standardy dla każdego obszaru branży ubezpieczeniowej. Główne jakie warto wiedzieć dla programisty pracującego w tej branży to:
Standardy ACORD według obszaru
| Standard | Obszar | Format | Notatki |
|---|---|---|---|
| ACORD XML (Zasady i zasady) | Uszkodzenia: samochodu, mienia, odpowiedzialności cywilnej | XML-a | Najbardziej rozpowszechniony standard w Ameryce Północnej |
| ACORD GRLC | Reasekuracja i duże ryzyka handlowe | XML + JSON (generacja 2.0) | Odnowiona w 2025 r. z podejściem opartym na API |
| EBOT ACORD | Regulacja rachunkowości (księgowość i rozliczenia) | XML-a | Planowana aktualizacja Gen 2.0 |
| ACORD ECOT | Ruchy reklamacyjne | XML-a | Planowana aktualizacja Gen 2.0 |
| ACORD CRP | Umowa, ryzyko i rozliczenie wstępne | XML + JSON | Nowy standard GRLC Gen 2.0 |
| ACORD Życie XML | Gałąź Życie i zdrowie | XML-a | Norma ACORD 103 i powiązane |
Struktura wiadomości XML ACORD
Wiadomość XML ACORD ma dobrze zdefiniowaną strukturę hierarchiczną. Zrozumienie bloków podstawy i pierwszy krok do jakiejkolwiek integracji. Oto przykładowa wiadomość za złożenie wniosku o wycenę polisy ubezpieczenia samochodu (ACORD 165 Personal Auto Quote Request).
<?xml version="1.0" encoding="UTF-8"?>
<ACORD>
<!-- Header obbligatorio: identifica mittente, destinatario, versione -->
<SignonRq>
<SignonTransport>
<SignonRoleCd>Customer</SignonRoleCd>
<CustId>
<SPName>it.federicocalo.insurtech</SPName>
<CustPermId>broker-001</CustPermId>
</CustId>
</SignonTransport>
<ClientDt>2025-03-10T14:30:00</ClientDt>
<CustLangPref>it-IT</CustLangPref>
<ClientApp>
<Org>ACME InsurTech</Org>
<Name>PolicyPortal</Name>
<Version>2.1.0</Version>
</ClientApp>
</SignonRq>
<!-- Corpo del messaggio: richiesta di quotazione auto -->
<InsuranceSvcRq>
<RqUID>a1b2c3d4-e5f6-7890-abcd-ef1234567890</RqUID>
<SPName>it.federicocalo.insurtech</SPName>
<RqDt>2025-03-10</RqDt>
<PersAutoPolicyQuoteInqRq>
<RqUID>quote-2025-001234</RqUID>
<CurCd>EUR</CurCd>
<QuoteInfo>
<EffectiveDt>2025-04-01</EffectiveDt>
<ExpirationDt>2026-03-31</ExpirationDt>
</QuoteInfo>
<!-- Informazioni polizza -->
<PersPolicy>
<LOBCd>AUTO</LOBCd>
<ContractTerm>
<DurationPeriod>
<NumUnits>12</NumUnits>
<UnitMeasurementCd>MON</UnitMeasurementCd>
</DurationPeriod>
</ContractTerm>
</PersPolicy>
<!-- Assicurato principale -->
<InsuredOrPrincipal>
<GeneralPartyInfo>
<NameInfo>
<PersonName>
<GivenName>Mario</GivenName>
<Surname>Rossi</Surname>
</PersonName>
</NameInfo>
<Addr>
<Addr1>Via Roma 1</Addr1>
<City>Milano</City>
<StateProvCd>MI</StateProvCd>
<PostalCode>20121</PostalCode>
<CountryCd>IT</CountryCd>
</Addr>
</GeneralPartyInfo>
<InsuredOrPrincipalInfo>
<InsuredOrPrincipalRoleCd>Insured</InsuredOrPrincipalRoleCd>
<PersonInfo>
<BirthDt>1985-06-15</BirthDt>
<GenderCd>M</GenderCd>
<LicensedDt>2003-09-20</LicensedDt>
</PersonInfo>
</InsuredOrPrincipalInfo>
</InsuredOrPrincipal>
<!-- Veicolo -->
<PersAutoLineBusiness>
<PersVeh>
<VehNumber>1</VehNumber>
<ModelYr>2022</ModelYr>
<Manufacturer>FIAT</Manufacturer>
<Model>Tipo</Model>
<VehIdentificationNumber>ZFA35600006G12345</VehIdentificationNumber>
<EstimatedAnnualDistance>
<NumUnits>15000</NumUnits>
<UnitMeasurementCd>KMT</UnitMeasurementCd>
</EstimatedAnnualDistance>
<Coverage>
<CoverageCd>RCA</CoverageCd>
<CoverageDesc>Responsabilità Civile Auto</CoverageDesc>
</Coverage>
<Coverage>
<CoverageCd>KASKO</CoverageCd>
<CoverageDesc>Kasko Completo</CoverageDesc>
<Deductible>
<FormatInteger>500</FormatInteger>
<CurCd>EUR</CurCd>
</Deductible>
</Coverage>
</PersVeh>
</PersAutoLineBusiness>
</PersAutoPolicyQuoteInqRq>
</InsuranceSvcRq>
</ACORD>
Wiadomość ACORD w formacie JSON (GRLC Gen 2.0)
Nowy standard GRLC Generation 2.0 (2025) wprowadza natywną reprezentację JSON komunikaty reasekuracyjne i duże ryzyko handlowe. Format JSON jest lżejszy, łatwiejszy do przetwarzania w nowoczesnych architekturach (REST API, streaming zdarzeń) i tak integruje się natywnie z narzędziami takimi jak OpenAPI i JSON Schema.
{
"acordVersion": "GRLC-2.0",
"messageId": "msg-2025-03-10-001234",
"messageType": "RiskSubmission",
"timestamp": "2025-03-10T14:30:00Z",
"sender": {
"organizationId": "broker-ACME-001",
"name": "ACME Broker SpA",
"role": "Broker"
},
"receiver": {
"organizationId": "insurer-XYZ-IT",
"name": "XYZ Assicurazioni SpA",
"role": "Insurer"
},
"risk": {
"riskId": "RISK-2025-IT-00456",
"lineOfBusiness": "Marine",
"inceptionDate": "2025-04-01",
"expiryDate": "2026-03-31",
"currency": "EUR",
"insuredObject": {
"type": "Vessel",
"name": "MV Adriatico",
"imoNumber": "9876543",
"vesselType": "BulkCarrier",
"grossTonnage": 45000,
"yearBuilt": 2018,
"flag": "IT",
"value": 25000000.00
},
"coverages": [
{
"coverageType": "HullAndMachinery",
"insuredValue": 25000000.00,
"deductible": 100000.00,
"conditions": "Institute Cargo Clauses (A)"
},
{
"coverageType": "ProtectionAndIndemnity",
"limit": 100000000.00,
"conditions": "Standard P&I Terms"
}
],
"locations": [
{
"locationType": "PrimaryPort",
"portName": "Genova",
"country": "IT",
"coordinates": {
"latitude": 44.4071,
"longitude": 8.9342
}
}
]
},
"submission": {
"requestedCapacity": 10000000.00,
"requestedShare": 0.4,
"expiringPremium": 185000.00,
"desiredPremium": 190000.00,
"submissionNotes": "Rinnovo con storico sinistri pulito. Certificato classe 1A Lloyd's Register."
}
}
Warstwa mapowania: od modelu autorskiego do ACORD
Rzadko kiedy wewnętrzne systemy firm bezpośrednio korzystają z nomenklatury ACORD. Warstwa mapowania przekłada się pomiędzy autorskim modelem danych a standardem ACORD, dwukierunkowo. Warstwa ta musi być na bieżąco aktualizowana o wersje standardów.
from dataclasses import dataclass
from datetime import date
from typing import Dict, List, Optional, Any
from enum import Enum
import xml.etree.ElementTree as ET
from xml.dom import minidom
import json
# ============ Modello dati INTERNO (proprietario) ============
class CoverageType(str, Enum):
LIABILITY = "RCA"
COMPREHENSIVE = "KASKO"
FIRE_THEFT = "INCENDIO_FURTO"
WINDSHIELD = "CRISTALLI"
DRIVER_ACCIDENT = "INFORTUNI_CONDUCENTE"
@dataclass
class InternalVehicle:
"""Modello veicolo del sistema interno."""
plate: str
vin: str
brand: str
model: str
year: int
engine_cc: int
fuel_type: str # benzina, diesel, elettrico, ibrido
annual_km: int
@dataclass
class InternalPolicyholder:
"""Modello assicurato del sistema interno."""
tax_code: str # codice fiscale
first_name: str
last_name: str
birth_date: date
gender: str # M/F
address: str
city: str
province: str
postal_code: str
license_date: date
claims_3yr: int
@dataclass
class InternalQuoteRequest:
"""Richiesta di quotazione nel formato del sistema interno."""
request_id: str
policyholder: InternalPolicyholder
vehicle: InternalVehicle
coverages: List[CoverageType]
effective_date: date
annual_mileage: int
garage_type: str # "privato", "condominiale", "pubblico"
# ============ Mapper ACORD ============
class ACORDMapper:
"""
Mappa i modelli dati interni ai messaggi ACORD XML/JSON.
Supporta:
- ACORD 165: Personal Auto Quote Request (XML)
- ACORD GRLC 2.0: Risk Submission (JSON)
"""
# Mapping codici interni -> codici ACORD standard
GENDER_MAP: Dict[str, str] = {
"M": "M", "F": "F", "ALTRO": "O", "NON_SPECIFICATO": "U"
}
FUEL_MAP: Dict[str, str] = {
"benzina": "GAS", "diesel": "DIE", "elettrico": "ELE",
"ibrido": "HYB", "gpl": "LPG", "metano": "CNG"
}
GARAGE_MAP: Dict[str, str] = {
"privato": "PRIV", "condominiale": "COND", "pubblico": "PUB",
"nessuno": "NONE"
}
COVERAGE_MAP: Dict[CoverageType, Dict[str, str]] = {
CoverageType.LIABILITY: {"code": "BI", "desc": "Bodily Injury Liability"},
CoverageType.COMPREHENSIVE: {"code": "COMP", "desc": "Comprehensive"},
CoverageType.FIRE_THEFT: {"code": "FT", "desc": "Fire and Theft"},
CoverageType.WINDSHIELD: {"code": "GLAS", "desc": "Glass Coverage"},
CoverageType.DRIVER_ACCIDENT: {"code": "PACC", "desc": "Personal Accident"},
}
def to_acord_xml(self, request: InternalQuoteRequest) -> str:
"""
Converte una richiesta interna nel formato ACORD 165 XML.
Returns: stringa XML formattata e pretty-printed.
"""
root = ET.Element("ACORD")
# Header
signon_rq = ET.SubElement(root, "SignonRq")
transport = ET.SubElement(signon_rq, "SignonTransport")
role = ET.SubElement(transport, "SignonRoleCd")
role.text = "Customer"
client_dt = ET.SubElement(signon_rq, "ClientDt")
client_dt.text = date.today().isoformat()
# Body
svc_rq = ET.SubElement(root, "InsuranceSvcRq")
rq_uid = ET.SubElement(svc_rq, "RqUID")
rq_uid.text = request.request_id
quote_rq = ET.SubElement(svc_rq, "PersAutoPolicyQuoteInqRq")
# Valuta
cur = ET.SubElement(quote_rq, "CurCd")
cur.text = "EUR"
# Info quotazione
quote_info = ET.SubElement(quote_rq, "QuoteInfo")
eff_dt = ET.SubElement(quote_info, "EffectiveDt")
eff_dt.text = request.effective_date.isoformat()
# Assicurato
self._add_insured_xml(quote_rq, request.policyholder)
# Linea Auto
auto_line = ET.SubElement(quote_rq, "PersAutoLineBusiness")
self._add_vehicle_xml(auto_line, request.vehicle, request.coverages)
# Pretty print
xml_string = ET.tostring(root, encoding="unicode")
dom = minidom.parseString(xml_string)
return dom.toprettyxml(indent=" ", encoding=None)
def _add_insured_xml(self, parent: ET.Element, ph: InternalPolicyholder) -> None:
"""Aggiunge il blocco InsuredOrPrincipal al messaggio XML."""
insured = ET.SubElement(parent, "InsuredOrPrincipal")
party_info = ET.SubElement(insured, "GeneralPartyInfo")
name_info = ET.SubElement(party_info, "NameInfo")
person_name = ET.SubElement(name_info, "PersonName")
given = ET.SubElement(person_name, "GivenName")
given.text = ph.first_name
surname = ET.SubElement(person_name, "Surname")
surname.text = ph.last_name
addr = ET.SubElement(party_info, "Addr")
addr1 = ET.SubElement(addr, "Addr1")
addr1.text = ph.address
city_el = ET.SubElement(addr, "City")
city_el.text = ph.city
state = ET.SubElement(addr, "StateProvCd")
state.text = ph.province
postal = ET.SubElement(addr, "PostalCode")
postal.text = ph.postal_code
country = ET.SubElement(addr, "CountryCd")
country.text = "IT"
insured_info = ET.SubElement(insured, "InsuredOrPrincipalInfo")
role = ET.SubElement(insured_info, "InsuredOrPrincipalRoleCd")
role.text = "Insured"
person_info = ET.SubElement(insured_info, "PersonInfo")
birth = ET.SubElement(person_info, "BirthDt")
birth.text = ph.birth_date.isoformat()
gender = ET.SubElement(person_info, "GenderCd")
gender.text = self.GENDER_MAP.get(ph.gender, "U")
licensed = ET.SubElement(person_info, "LicensedDt")
licensed.text = ph.license_date.isoformat()
def _add_vehicle_xml(
self, parent: ET.Element, vehicle: InternalVehicle, coverages: List[CoverageType]
) -> None:
"""Aggiunge il blocco PersVeh al messaggio XML."""
veh = ET.SubElement(parent, "PersVeh")
num = ET.SubElement(veh, "VehNumber")
num.text = "1"
yr = ET.SubElement(veh, "ModelYr")
yr.text = str(vehicle.year)
manuf = ET.SubElement(veh, "Manufacturer")
manuf.text = vehicle.brand
model_el = ET.SubElement(veh, "Model")
model_el.text = vehicle.model
vin = ET.SubElement(veh, "VehIdentificationNumber")
vin.text = vehicle.vin
ann_dist = ET.SubElement(veh, "EstimatedAnnualDistance")
num_units = ET.SubElement(ann_dist, "NumUnits")
num_units.text = str(vehicle.annual_km)
unit_meas = ET.SubElement(ann_dist, "UnitMeasurementCd")
unit_meas.text = "KMT"
for cov_type in coverages:
cov_data = self.COVERAGE_MAP.get(cov_type, {"code": "UNKN", "desc": "Unknown"})
cov_el = ET.SubElement(veh, "Coverage")
cov_cd = ET.SubElement(cov_el, "CoverageCd")
cov_cd.text = cov_data["code"]
cov_desc = ET.SubElement(cov_el, "CoverageDesc")
cov_desc.text = cov_data["desc"]
def to_acord_json(self, request: InternalQuoteRequest) -> Dict[str, Any]:
"""
Converte la richiesta interna nel formato ACORD JSON (semplificato).
Per GRLC 2.0: usa il formato esteso con risk/coverage/submission structure.
"""
ph = request.policyholder
veh = request.vehicle
return {
"acordVersion": "PersonalLines-1.0",
"messageId": request.request_id,
"messageType": "AutoQuoteRequest",
"timestamp": date.today().isoformat(),
"policyholder": {
"taxCode": ph.tax_code,
"firstName": ph.first_name,
"lastName": ph.last_name,
"birthDate": ph.birth_date.isoformat(),
"gender": self.GENDER_MAP.get(ph.gender, "U"),
"licenseDate": ph.license_date.isoformat(),
"claimsLast3Years": ph.claims_3yr,
"address": {
"street": ph.address,
"city": ph.city,
"province": ph.province,
"postalCode": ph.postal_code,
"countryCode": "IT",
},
},
"vehicle": {
"registrationPlate": veh.plate,
"vin": veh.vin,
"manufacturer": veh.brand,
"model": veh.model,
"year": veh.year,
"engineCC": veh.engine_cc,
"fuelType": self.FUEL_MAP.get(veh.fuel_type, "UNKN"),
"annualMileageKm": veh.annual_km,
},
"requestedCoverages": [
self.COVERAGE_MAP.get(c, {"code": "UNKN", "desc": "Unknown"})["code"]
for c in request.coverages
],
"effectiveDate": request.effective_date.isoformat(),
"currency": "EUR",
}
Bramka API ACORD z FastAPI
Udostępnienie możliwości integracji ACORD poprzez REST API umożliwia brokerom, agencje i systemy innych firm, aby połączyć się z systemem ubezpieczeniowym bez konieczności zarządzania złożoność standardów XML wewnętrznie. Bramka tłumaczy wywołania REST na wiadomości ACORD i odwrotnie.
from fastapi import FastAPI, HTTPException, Depends, Header
from fastapi.responses import Response
from pydantic import BaseModel, Field
from typing import List, Optional
from datetime import date
import uuid
# ============ Pydantic Models per l'API REST ============
class VehicleRequest(BaseModel):
plate: str = Field(..., description="Targa del veicolo (formato italiano)")
vin: str = Field(..., min_length=17, max_length=17, description="VIN (17 caratteri)")
brand: str
model: str
year: int = Field(..., ge=1990, le=2030)
engine_cc: int = Field(..., ge=50, le=10000)
fuel_type: str = Field(..., description="benzina|diesel|elettrico|ibrido|gpl|metano")
annual_km: int = Field(..., ge=1000, le=150000)
class PolicyholderRequest(BaseModel):
tax_code: str = Field(..., min_length=16, max_length=16)
first_name: str
last_name: str
birth_date: date
gender: str = Field(..., pattern="^[MFO]$")
address: str
city: str
province: str = Field(..., min_length=2, max_length=2)
postal_code: str = Field(..., pattern=r"^\d{5}$")
license_date: date
claims_3yr: int = Field(default=0, ge=0, le=10)
class QuoteRequestBody(BaseModel):
policyholder: PolicyholderRequest
vehicle: VehicleRequest
coverages: List[str] = Field(..., min_items=1)
effective_date: date
response_format: str = Field(default="json", description="json|xml")
class QuoteResponse(BaseModel):
quote_id: str
request_id: str
status: str
estimated_annual_premium: Optional[float] = None
currency: str = "EUR"
valid_until: Optional[date] = None
breakdown: Optional[dict] = None
acord_message_id: Optional[str] = None
# ============ FastAPI Application ============
app = FastAPI(
title="Insurance ACORD API Gateway",
description="REST API layer per integrazione ACORD con sistemi assicurativi",
version="2.1.0",
docs_url="/api/v1/docs",
)
def verify_api_key(x_api_key: str = Header(...)) -> str:
"""Dependency per autenticazione API key."""
valid_keys = {"broker-001": "ACME Broker", "broker-002": "XYZ Agency"}
if x_api_key not in valid_keys:
raise HTTPException(status_code=401, detail="API key non valida")
return valid_keys[x_api_key]
@app.post(
"/api/v1/quotes/personal-auto",
response_model=QuoteResponse,
summary="Richiesta quotazione polizza auto personale",
description="Genera una quotazione per polizza RC auto e garanzie accessorie",
)
async def create_auto_quote(
request: QuoteRequestBody,
client_name: str = Depends(verify_api_key),
) -> QuoteResponse:
"""
Endpoint per richiesta di quotazione polizza auto.
Converte la richiesta REST nel formato ACORD appropriato,
invia al motore di pricing interno e restituisce la quotazione.
"""
request_id = str(uuid.uuid4())
# Converti il body REST nel modello interno
internal_request = _build_internal_request(request, request_id)
# Genera il messaggio ACORD
mapper = ACORDMapper()
if request.response_format == "xml":
acord_message = mapper.to_acord_xml(internal_request)
else:
acord_message = mapper.to_acord_json(internal_request)
# Invia al motore di pricing (simulato)
pricing_result = await _call_pricing_engine(internal_request)
return QuoteResponse(
quote_id=str(uuid.uuid4()),
request_id=request_id,
status="QUOTED",
estimated_annual_premium=pricing_result.get("annual_premium"),
currency="EUR",
valid_until=date.today().replace(month=date.today().month % 12 + 1) if date.today().month < 12 else date.today().replace(year=date.today().year + 1, month=1),
breakdown=pricing_result.get("breakdown"),
acord_message_id=f"ACORD-{request_id[:8]}",
)
@app.post(
"/api/v1/quotes/personal-auto/xml",
response_class=Response,
summary="Richiesta quotazione in formato ACORD XML",
)
async def create_auto_quote_xml(
request: QuoteRequestBody,
client_name: str = Depends(verify_api_key),
) -> Response:
"""Restituisce direttamente il messaggio ACORD XML della quotazione."""
request_id = str(uuid.uuid4())
internal_request = _build_internal_request(request, request_id)
mapper = ACORDMapper()
xml_content = mapper.to_acord_xml(internal_request)
return Response(content=xml_content, media_type="application/xml")
@app.get(
"/api/v1/health",
summary="Health check del gateway",
)
async def health_check() -> dict:
return {
"status": "healthy",
"version": "2.1.0",
"acord_version": "ACORD-XML-165 / GRLC-2.0",
}
def _build_internal_request(request: QuoteRequestBody, request_id: str) -> InternalQuoteRequest:
"""Converte il body REST nel modello interno."""
from datetime import timedelta
ph = request.policyholder
veh = request.vehicle
return InternalQuoteRequest(
request_id=request_id,
policyholder=InternalPolicyholder(
tax_code=ph.tax_code,
first_name=ph.first_name,
last_name=ph.last_name,
birth_date=ph.birth_date,
gender=ph.gender,
address=ph.address,
city=ph.city,
province=ph.province,
postal_code=ph.postal_code,
license_date=ph.license_date,
claims_3yr=ph.claims_3yr,
),
vehicle=InternalVehicle(
plate=veh.plate,
vin=veh.vin,
brand=veh.brand,
model=veh.model,
year=veh.year,
engine_cc=veh.engine_cc,
fuel_type=veh.fuel_type,
annual_km=veh.annual_km,
),
coverages=[CoverageType(c) for c in request.coverages if c in CoverageType.__members__.values()],
effective_date=request.effective_date,
annual_mileage=veh.annual_km,
garage_type="privato",
)
async def _call_pricing_engine(request: InternalQuoteRequest) -> dict:
"""Simulazione chiamata al motore di pricing interno."""
# In produzione: chiamata HTTP al microservizio di pricing
base_premium = 450.0
if request.policyholder.claims_3yr > 0:
base_premium *= (1 + request.policyholder.claims_3yr * 0.3)
if request.vehicle.year < 2015:
base_premium *= 1.1
return {
"annual_premium": round(base_premium, 2),
"breakdown": {
"RCA": round(base_premium * 0.65, 2),
"KASKO": round(base_premium * 0.25, 2),
"accessories": round(base_premium * 0.10, 2),
},
}
Walidacja wiadomości ACORD za pomocą schematu JSON
Weryfikacja wiadomości ma kluczowe znaczenie przed wysłaniem lub przetworzeniem jakiejkolwiek wiadomości ACORD. Zniekształcona wiadomość może spowodować ciche odrzucenie przez system odbierający lub, co gorsza, błędy w danych dotyczących polisy lub roszczenia. Schemat JSON jest standardowym narzędziem do walidacji wiadomości JSON ACORD.
import jsonschema
from jsonschema import validate, ValidationError
import json
from typing import Dict, List, Any, Tuple
# Schema JSON per una Quote Request ACORD semplificata
ACORD_AUTO_QUOTE_SCHEMA: Dict[str, Any] = {
"$schema": "https://json-schema.org/draft/2020-12/schema",
"$id": "https://acord.org/schemas/personal-auto-quote-request-v1",
"title": "ACORD Personal Auto Quote Request",
"type": "object",
"required": [
"acordVersion", "messageId", "messageType", "timestamp",
"policyholder", "vehicle", "requestedCoverages", "effectiveDate", "currency"
],
"properties": {
"acordVersion": {
"type": "string",
"pattern": "^[A-Za-z0-9.-]+$"
},
"messageId": {"type": "string", "minLength": 1},
"messageType": {
"type": "string",
"enum": ["AutoQuoteRequest", "AutoPolicyIssue", "ClaimNotification"]
},
"timestamp": {"type": "string", "format": "date-time"},
"policyholder": {
"type": "object",
"required": ["firstName", "lastName", "birthDate", "gender", "licenseDate"],
"properties": {
"firstName": {"type": "string", "minLength": 1},
"lastName": {"type": "string", "minLength": 1},
"birthDate": {"type": "string", "format": "date"},
"gender": {"type": "string", "enum": ["M", "F", "O", "U"]},
"licenseDate": {"type": "string", "format": "date"},
"claimsLast3Years": {"type": "integer", "minimum": 0, "maximum": 10},
"address": {
"type": "object",
"required": ["city", "countryCode"],
"properties": {
"street": {"type": "string"},
"city": {"type": "string"},
"postalCode": {"type": "string"},
"countryCode": {"type": "string", "pattern": "^[A-Z]{2}$"},
}
}
}
},
"vehicle": {
"type": "object",
"required": ["manufacturer", "model", "year", "fuelType", "annualMileageKm"],
"properties": {
"vin": {"type": "string", "pattern": "^[A-HJ-NPR-Z0-9]{17}$"},
"manufacturer": {"type": "string"},
"model": {"type": "string"},
"year": {"type": "integer", "minimum": 1980, "maximum": 2030},
"fuelType": {
"type": "string",
"enum": ["GAS", "DIE", "ELE", "HYB", "LPG", "CNG", "UNKN"]
},
"annualMileageKm": {"type": "integer", "minimum": 100, "maximum": 200000}
}
},
"requestedCoverages": {
"type": "array",
"items": {"type": "string"},
"minItems": 1
},
"effectiveDate": {"type": "string", "format": "date"},
"currency": {"type": "string", "pattern": "^[A-Z]{3}$"}
}
}
class ACORDMessageValidator:
"""Validatore di messaggi ACORD con JSON Schema."""
def __init__(self) -> None:
self.validator = jsonschema.Draft202012Validator(ACORD_AUTO_QUOTE_SCHEMA)
def validate(self, message: Dict) -> Tuple[bool, List[str]]:
"""
Valida un messaggio ACORD.
Returns:
(is_valid, error_messages): tuple con risultato e lista di errori
"""
errors: List[str] = []
try:
self.validator.validate(message)
return True, []
except ValidationError as e:
# Raccogli tutti gli errori, non solo il primo
for error in self.validator.iter_errors(message):
path = " -> ".join(str(p) for p in error.absolute_path)
errors.append(f"Campo '{path}': {error.message}")
return False, errors
def validate_strict(self, message_json: str) -> Tuple[bool, List[str]]:
"""Valida da stringa JSON con parsing incluso."""
try:
message = json.loads(message_json)
except json.JSONDecodeError as e:
return False, [f"JSON malformato: {e}"]
return self.validate(message)
# Esempio di utilizzo
if __name__ == "__main__":
validator = ACORDMessageValidator()
valid_message = {
"acordVersion": "PersonalLines-1.0",
"messageId": "msg-001",
"messageType": "AutoQuoteRequest",
"timestamp": "2025-03-10T14:30:00Z",
"policyholder": {
"firstName": "Mario",
"lastName": "Rossi",
"birthDate": "1985-06-15",
"gender": "M",
"licenseDate": "2003-09-20",
"address": {"city": "Milano", "countryCode": "IT"},
},
"vehicle": {
"manufacturer": "Fiat",
"model": "Tipo",
"year": 2022,
"fuelType": "GAS",
"annualMileageKm": 15000,
},
"requestedCoverages": ["BI", "COMP"],
"effectiveDate": "2025-04-01",
"currency": "EUR",
}
is_valid, errors = validator.validate(valid_message)
print(f"Valido: {is_valid}, Errori: {errors}")
# Output: Valido: True, Errori: []
Wzór integracji ubezpieczeń
Wybór wzorca integracji zależy od ilości komunikatów i wymaganego opóźnienia i złożoność ekosystemu. Oto główne wzorce panujące w branży ubezpieczeniowej:
Porównanie wzorców integracji
| Wzory | Kiedy go używać | Zalety | Wady |
|---|---|---|---|
| API typu punkt-punkt | Niewiele integracji, niski wolumen | Prostota, niskie opóźnienia | Bez skalowania, ścisłe połączenie |
| ESB (korporacyjna magistrala usług) | Wiele heterogenicznych systemów, duże firmy | Scentralizowana orkiestracja, transformacje | Pojedynczy punkt awarii, złożoność |
| Oparte na zdarzeniach (Kafka) | Mikrousługi o dużej objętości, w czasie rzeczywistym | Skalowalność, oddzielenie, ścieżka audytu | Złożoność operacyjna, ostateczna spójność |
| Brama API + adapter | Nowoczesny, natywny dla chmury InsurTech | Standaryzacja, wersjonowanie, bezpieczeństwo | Dodatkowe opóźnienia, brama jako wąskie gardło |
Najlepsze praktyki i anty-wzorce
Najlepsze praktyki dotyczące integracji z ACORD
- Komunikaty wersji: w wiadomości zawsze dołączaj wersję standardu ACORD (np. „acordVersion”: „GRLC-2.0”); Kompatybilność wsteczna jest krytyczna
- Obowiązkowa idempotencja: wykorzystuje RqUID (unikalny identyfikator żądania), aby mieć pewność, że zduplikowane wiadomości nie generują zduplikowanych transakcji
- Ważne przed wysłaniem: zawsze sprawdzaj wiadomość za pomocą schematu XSD/JSON po stronie nadawcy przed jej wysłaniem — nie polegaj na błędzie odbiorcy
- Pełny dziennik wiadomości: przechowuj kopię wszystkich wysłanych i otrzymanych wiadomości ACORD przez co najmniej 10 lat (obowiązkowe ze względu na przepisy ubezpieczeniowe)
- Przekrocz limit czasu i spróbuj ponownie z wykładniczym wycofywaniem: integracje ubezpieczeniowe muszą radzić sobie z chwilowymi awariami; zaimplementuj ponowną próbę z jitterem, aby uniknąć grzmotu stada
Anty-wzorce, których należy unikać
- Mapowania zakodowane na stałe: nie koduj na stałe map kodu ACORD w kodzie aplikacji; przechowuj je w plikach konfiguracyjnych w wersji i z możliwością aktualizacji bez konieczności wdrażania
- Ignoruj kody odpowiedzi: ACORD zapewnia standardowe komunikaty odpowiedzi; Zawsze traktuj kody błędów (MsgStatusCd), a nie tylko kody HTTP
- Transformacje w BSE: przechowuj transformacje danych w warstwie aplikacji, a nie w ESB; ESB musi być routerem, a nie procesorem danych
- Użyj ACORD XML w nowych systemach: w przypadku nowych integracji preferuj ACORD JSON (GRLC 2.0) lub REST API, które firma-odbiorca udostępnia na ACORD
Wnioski i dalsze kroki
Standardy ACORD są językiem interoperacyjności ubezpieczeń. Zrozum ich a wiedza o tym, jak poprawnie je wdrożyć, jest podstawową umiejętnością każdego programisty którzy pracują w branży InsurTech, zarówno po stronie firmy, jak i po stronie brokera lub agregatora.
GRLC Generation 2.0 na rok 2025 oznacza zmianę zasad gry: natywną obsługę JSON i Podejście oparte na API wreszcie sprawia, że standardy ACORD są dostępne dla programistów współcześni użytkownicy przyzwyczajeni do REST i JSON, znacznie obniżając barierę wejścia.
Następny i ostatni artykuł z tej serii poświęcony jest Inżynieria zgodności: Wypłacalność II i MSSF 17 — jak budować infrastrukturę danych i potoki raportowania niezbędne do spełnienia najbardziej rygorystycznych obowiązków regulacyjnych sektora ubezpieczeniowego.
Seria inżynieryjna InsurTech
- 01 - Domena ubezpieczeniowa dla programistów: produkty, aktorzy i modele danych
- 02 — Zarządzanie polityką w chmurze: architektura oparta na interfejsie API
- 03 – Rurociąg telematyczny: przetwarzanie danych UBI na dużą skalę
- 04 - Underwriting AI: Inżynieria funkcji i punktacja ryzyka
- 05 - Automatyzacja roszczeń: wizja komputerowa i NLP
- 06 - Wykrywanie oszustw: analiza wykresów i sygnał behawioralny
- 07 - Integracja standardu ACORD i API ubezpieczeń (ten artykuł)
- 08 - Inżynieria zgodności: Wypłacalność II i MSSF 17







