ACORD 표준 및 보험 API 통합
보험 생태계는 그 성격상 보험 회사, 재보험사, 중개인, 대리점, 은행(방카슈랑스), 온라인 비교업체, 공급업체 서비스(워크샵, 병원, 변호사) — 이 모든 행위자는 데이터를 교환해야 합니다. 효율적이고 정확하며 감사 가능합니다. 공유 표준이 없으면 모든 통합이 비용과 위험이 높은 맞춤형 프로젝트가 됩니다.
어코드 (협력운영연구개발협회) 보험 산업의 데이터 표준을 설정하는 글로벌 조직입니다. 이후 1970. 표준 — 정책, 청구, 관리를 위한 XML 및 JSON 메시지 재보험 및 회계 규정은 처음부터 채택되었습니다. 36,000개 조직 100개국에서. 2025년 ACORD는 i를 출시했습니다. GRLC 2.0세대 데이터 표준, 재보험 표준과 대규모 상업 위험을 종합적으로 검토합니다. 기본 JSON 지원 및 API 우선 방향.
이 가이드는 ACORD 표준을 구체적인 코드로 변환합니다. 메시지를 구성하는 방법 표준을 준수하는 XML/JSON, 독점 시스템 간 매핑 계층을 구축하는 방법 및 ACORD, 그리고 업계 모범 사례를 준수하는 RESTful API를 노출하는 방법을 알아보세요.
무엇을 배울 것인가
- ACORD 표준 개요: 다양한 사업 부문을 위한 XML 및 JSON
- 정책 및 청구에 대한 ACORD 메시지의 구조
- 독점 데이터 모델과 ACORD 간의 매핑
- Python/FastAPI에서 ACORD 호환 API 게이트웨이 구현
- XSD(XML 스키마) 및 JSON 스키마를 사용한 메시지 유효성 검사
- 통합 패턴: 지점 간, ESB, 이벤트 중심
- ACORD 인증: 요구 사항 및 테스트 프로세스
ACORD 표준 개요
ACORD는 보험 산업의 각 영역에 대해 서로 다른 표준을 발표합니다. 주요 내용 해당 분야에서 일하는 개발자가 알아야 할 사항은 다음과 같습니다.
지역별 ACORD 표준
| 기준 | 영역 | 체재 | 메모 |
|---|---|---|---|
| ACORD XML(P&C) | 피해: 자동차, 재산, 책임 | XML | 북미에서 가장 널리 사용되는 표준 |
| 어코드 GRLC | 재보험 및 대규모 상업 위험 | XML + JSON(2.0세대) | API 우선 접근 방식으로 2025년 갱신 |
| 어코드 에봇 | 회계규정(회계 및 결산) | XML | Gen 2.0 업데이트 예정 |
| 어코드 에코트 | 청구 움직임 | XML | Gen 2.0 업데이트 예정 |
| 어코드 CRP | 계약, 위험 및 사전 회계 | XML + JSON | 새로운 GRLC Gen 2.0 표준 |
| ACORD 생활 XML | 생명건강분야 | XML | ACORD 표준 103 및 관련 |
ACORD XML 메시지의 구조
ACORD XML 메시지에는 잘 정의된 계층 구조가 있습니다. 블록 이해 모든 통합을 위한 기본이자 첫 번째 단계입니다. 다음은 예시 메시지입니다. 자동차 보험 견적 요청(ACORD 165 개인 자동 견적 요청)
<?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>
JSON 형식의 ACORD 메시지(GRLC Gen 2.0)
새로운 GRLC 2.0세대(2025) 표준은 다음을 위한 기본 JSON 표현을 도입합니다. 재보험 메시지와 대규모 상업적 위험. JSON 형식은 더 가볍습니다. 최신 아키텍처(REST API, 이벤트 스트리밍)에서 처리하기가 더 쉽고 그렇습니다. OpenAPI 및 JSON 스키마와 같은 도구와 기본적으로 통합됩니다.
{
"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."
}
}
매핑 레이어: 독점 모델에서 ACORD까지
회사 내부 시스템에서 ACORD 명명법을 직접 사용하는 경우는 거의 없습니다. 매핑 계층은 독점 데이터 모델과 ACORD 표준 간을 변환합니다. 양방향으로. 이 레이어는 표준 버전으로 계속 업데이트되어야 합니다.
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",
}
FastAPI를 갖춘 ACORD API 게이트웨이
REST API를 통해 ACORD 통합 기능을 노출하면 브로커가 별도의 관리 없이 보험 시스템에 연결할 수 있는 기관 및 제3자 시스템 내부적으로 XML 표준이 복잡합니다. 게이트웨이는 REST 호출을 메시지로 변환합니다. ACORD와 그 반대.
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),
},
}
JSON 스키마를 사용하여 ACORD 메시지 유효성 검사
ACORD 메시지를 보내거나 처리하기 전에 메시지 유효성 검사가 중요합니다. 잘못된 형식의 메시지는 수신 시스템에서 자동 거부를 유발할 수 있으며, 더 나쁜 경우에는 정책 또는 청구 데이터의 오류. JSON 스키마는 검증을 위한 표준 도구입니다. 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: []
보험 통합 패턴
통합 패턴 선택은 메시지 양과 필요한 대기 시간에 따라 달라집니다. 그리고 생태계의 복잡성. 보험업계의 주요 패턴은 다음과 같습니다.
통합 패턴 비교
| 패턴 | 언제 사용하는가 | 장점 | 단점 |
|---|---|---|---|
| 지점간 API | 몇 가지 통합, 적은 양 | 단순성, 낮은 대기 시간 | 비스케일링, 긴밀한 결합 |
| ESB(엔터프라이즈 서비스 버스) | 다수의 이기종 시스템, 대기업 | 중앙 집중식 오케스트레이션, 변환 | 단일 실패 지점, 복잡성 |
| 이벤트 중심(Kafka) | 대용량, 실시간, 마이크로서비스 | 확장성, 분리, 감사 추적 | 운영 복잡성, 최종 일관성 |
| API 게이트웨이 + 어댑터 | 현대적인 클라우드 기반 InsurTech | 표준화, 버전 관리, 보안 | 추가 대기 시간, 병목 현상이 발생하는 게이트웨이 |
모범 사례 및 안티패턴
ACORD 통합 모범 사례
- 버전 메시지: 항상 메시지에 ACORD 표준 버전을 포함하세요(예: "acordVersion": "GRLC-2.0"). 이전 버전과의 호환성이 중요합니다
- 필수 멱등성: 중복 메시지가 중복 트랜잭션을 생성하지 않도록 RqUID(고유 ID 요청)를 사용합니다.
- 보내기 전에 유효함: 메시지를 보내기 전에 항상 보낸 사람 측에서 XSD/JSON 스키마를 사용하여 메시지의 유효성을 검사하세요. 받는 사람의 오류에 의존하지 마세요.
- 전체 메시지 로그: 최소 10년 동안 주고받은 모든 ACORD 메시지의 사본을 보관합니다(보험 규정에 따라 필수).
- 지수 백오프를 사용한 시간 초과 및 재시도: 보험 통합은 일시적인 오류를 처리해야 합니다. 천둥소리를 피하기 위해 지터를 사용한 재시도 구현
피해야 할 안티패턴
- 하드코딩된 매핑: ACORD 코드 맵을 애플리케이션 코드에 하드코딩하지 마세요. 배포 없이 버전이 지정되고 업데이트 가능한 구성 파일에 보관합니다.
- 응답 코드 무시: ACORD는 표준화된 응답 메시지를 제공합니다. 항상 HTTP 코드뿐만 아니라 오류 코드(MsgStatusCd)도 처리하십시오.
- BSE의 변화: ESB가 아닌 애플리케이션 계층에서 데이터 변환을 유지합니다. ESB는 데이터 프로세서가 아닌 라우터여야 합니다.
- 새로운 시스템에는 ACORD XML을 사용하세요. 새로운 통합의 경우 ACORD JSON(GRLC 2.0) 또는 수신 회사가 ACORD 위에 노출하는 REST API를 선호합니다.
결론 및 다음 단계
ACORD 표준은 보험 상호 운용성의 공통어입니다. 그들을 이해하라 이를 올바르게 구현하는 방법을 아는 것은 모든 개발자의 기본 기술입니다. InsurTech 분야에서 회사 측과 브로커 또는 애그리게이터 측에서 일하는 사람입니다.
2025년 GRLC 2.0세대는 게임 체인저를 의미합니다. 기본 JSON 지원 및 API 우선 접근 방식을 통해 마침내 개발자가 ACORD 표준에 액세스할 수 있게 되었습니다. REST와 JSON에 익숙한 현대 사용자는 진입 장벽을 크게 낮췄습니다.
시리즈의 다음 기사이자 마지막 기사에서는 규정 준수 엔지니어링: Solvency II 및 IFRS 17 — 데이터 인프라 및 보고 파이프라인을 구축하는 방법 보험 부문의 가장 까다로운 규제 의무를 충족하는 데 필요합니다.
InsurTech 엔지니어링 시리즈
- 01 - 개발자를 위한 보험 도메인: 제품, 행위자 및 데이터 모델
- 02 - 클라우드 네이티브 정책 관리: API 우선 아키텍처
- 03 - 텔레매틱스 파이프라인: 대규모 UBI 데이터 처리
- 04 - AI Underwriting: 기능 엔지니어링 및 위험 평가
- 05 - 청구 자동화: 컴퓨터 비전 및 NLP
- 06 - 사기 탐지: 그래프 분석 및 행동 신호
- 07 - ACORD 표준 및 보험 API 통합(이 문서)
- 08 - 규정 준수 엔지니어링: Solvency II 및 IFRS 17







