pp-iapi-contracts
v1.0.0
Published
Kontrakt komunikacji ICC (api-v3) z workerami ubezpieczeniowymi: schematy zod zadan i wynikow, typy slownikowe oraz fabryka komend pp-command-bus
Downloads
474
Maintainers
Readme
pp-iapi-contracts
Kontrakt komunikacji między ICC (api-v3) a workerami ubezpieczeniowymi. Jedno źródło prawdy
dla obu stron kanału pp-command-bus: schematy zod zadań i wyników, typy słownikowe oraz
fabryka klas komend.
Paczka zawiera wyłącznie opis kontraktu. Nie ma tu logiki workera, Walk/Step, klienta HTTP,
debugowania ani cache - to zakres pp-worker-kernel.
Instalacja
npm install pp-iapi-contractsZależności równorzędne (musi je dostarczyć projekt konsumujący):
{
"peerDependencies": {
"pp-command-bus": "^2.0.0",
"zod": "^3.23.8"
}
}Zasada nadrzędna: zod jest źródłem, typy są pochodną
Każdy typ opisujący dane kontraktu pochodzi z z.infer. Nie utrzymujemy równolegle interfejsu
TypeScript i schematu zod dla tego samego bytu, bo dwa źródła zawsze się rozjeżdżają - a rozjazd
jest dokładnie tym problemem, który ta paczka likwiduje.
export const proposalTaskSchema = z.object({ payload: /* ... */ });
export type ProposalTask = z.infer<typeof proposalTaskSchema>;Wyjątkiem są trzy typy, które nie opisują danych kontraktu, tylko API programistyczne paczki:
JsonValue (typ rekurencyjny, którego zod 3 nie potrafi wyprowadzić bez adnotacji),
CommandDefinition i InsurerCommands (typy konstruktorów komend).
Skąd wzięły się kształty schematów
Schematy nie są przepisane z modeli jednego workera. Zostały wyprowadzone z modeli
worker-interrisk-iapi, a następnie skalibrowane na korpusie realnego ruchu produkcyjnego:
5709 payloadów z 9 towarzystw (balcia, hestia, interrisk, interriskIrys, link4, mtu24,
ominimo, trasti, tuz, uniqa) pobranych z katalogu debug stack-iapi.
Kalibracja wykryła w modelach workera braki, których nie widać było w kodzie:
| Znalezisko | Skala w korpusie |
|---|---|
| subjects[].livingPostalCode - brak w modelu i w walidatorze | 39,7% payloadów proposal |
| teryt.poczta - brak w modelu Teryt | 94,7% obiektów teryt |
| risks[].subRisk[].code, risks[].name - brak w modelach wyniku | 3073 wystąpienia |
| variants[].subjectList[] z płaskim adresem zamiast address | 354 wystąpienia |
| insuranceDetails.paymentSettings w ofercie (model ma je tylko w polisie) | 314 wystąpień |
| previousData.customization, customizationSchema, insurerVariant | 120 wystąpień |
| externalOfferId2, documentList[].fileCloudId | 20 wystąpień |
Reguła wymagalności: pole jest wymagane w kontrakcie tylko wtedy, gdy jest wymagane w modelu źródłowym i występuje w 100% payloadów korpusu. Jeżeli pole jest wymagane wyłącznie dlatego, że potrzebuje go API konkretnego towarzystwa - w kontrakcie jest opcjonalne, a wymóg towarzystwa zostaje w workerze jako osobna walidacja uruchamiana po walidacji kontraktowej.
Dlaczego pola opcjonalne są nullish, a nie optional
ICC używa null i pominięcia pola wymiennie. W korpusie null pojawia się w tysiącach miejsc
(subRisk.premium.value - 3039 razy, całe previousData, teryt.poczta, assignmentDetails,
customization). Schemat dopuszczający wyłącznie undefined odrzucał na tym korpusie
100% zadań oferty i aktualizacji kalkulacji.
Konsekwencja dla konsumenta: typy z tej paczki są szersze niż stare modele workerów - obsłuż
null tam, gdzie wcześniej TypeScript go nie pokazywał. To nie jest rozluźnienie kontraktu,
tylko usunięcie fikcji: worker dostawał null już wcześniej, po prostu nie było tego w typach.
Walidacja nie usuwa danych
Schematy używają domyślnego zachowania zod (strip), więc parse zwraca obiekt bez pól spoza
kontraktu. Jeżeli ICC doda pole, którego dana wersja paczki nie zna, worker po parse go nie
zobaczy. Do przekazywania danych dalej używaj oryginalnego payloadu, a wynik safeParse
traktuj jako werdykt walidacji. Test golden-file pilnuje, że dla realnych payloadów zbiór
usuwanych pól jest pusty - każde nowe pole ICC ujawni się jako czerwony test, a nie jako cicha
utrata danych na produkcji.
Zalecenie dla projektu konsumującego
Deklaracje typów wyprowadzone z zod są obszerne - typ ZodObject niesie strukturę schematu
trzykrotnie (kształt, wejście, wyjście). Ustaw w swoim tsconfig.json:
{
"compilerOptions": {
"skipLibCheck": true
}
}TypeScript pominie wtedy sprawdzanie deklaracji z node_modules, co skraca czas kompilacji
i podpowiedzi w edytorze. Nie wpływa to na sprawdzanie typów w Twoim kodzie - użycie schematów
i typów z paczki jest walidowane normalnie.
Eksporty
Zadania (ICC -> worker)
| Schemat | Typ |
|---|---|
| cepikTaskSchema | CepikTask |
| proposalTaskSchema | ProposalTask |
| proposalUpdateTaskSchema | ProposalUpdateTask |
| offerTaskSchema | OfferTask |
| policyTaskSchema | PolicyTask |
| taskSchema | Task (unia pięciu powyższych) |
Modele zadań zaczynają się od payload. Pola koperty transportowej RabbitMQ (__time,
__name, __id, __eventStore, __eventChain) nie należą do kontraktu - w pp-command-bus
są kopertą komendy, a dane biznesowe siedzą w __payload.
Wyniki (worker -> ICC)
| Schemat | Typ |
|---|---|
| cepikTaskResultSchema | CepikTaskResult |
| proposalTaskResultSchema | ProposalTaskResult |
| proposalUpdateTaskResultSchema | ProposalUpdateTaskResult |
| offerTaskResultSchema | OfferTaskResult |
| policyTaskResultSchema | PolicyTaskResult |
| taskResultSchema | TaskResult (unia pięciu powyższych) |
Schematy wspólne
messageSchema, credentialsSchema, getCredentialsResponseSchema, terytSchema,
addressSchema, moneySchema, documentSchema, identifiedDocumentSchema, consentSchema,
identifiedConsentSchema, resultConsentSchema, offerFlagSchema, insurancePeriodSchema,
insuranceBrandSchema, assignmentDetailsSchema, infoEkspertDetailsSchema,
eurotaxVehicleSchema, eurotaxDetailsSchema, dictionaryVehicleDetailsSchema,
jsonValueSchema, jsonObjectSchema.
Słowniki
Każdy słownik eksportuje trzy rzeczy pod jedną nazwą bytu: obiekt wartości, schemat i typ.
import { MessageType, messageTypeSchema } from 'pp-iapi-contracts';
MessageType.Error; // 'ERROR'
messageTypeSchema.safeParse('ERROR').success; // true
const type: MessageType = MessageType.Error;Dostępne słowniki: AssistanceVariantType, CepikOwnerType, CompanyType,
CreationLocationType, CurrencyType, DocumentType, FirstRegistrationCountryCodeType,
InstallmentsNumberType, InsuranceSummaryType, LossAdjustmentType, ManualVehicleValueType,
MaritalStatusType, MessageSourceType, MessageType, NnwVariantType, OccupationType,
OfferFlagNameType, ParkingPlaceType, PaymentMethodType, ProcessStatusType, RiskNameType,
RiskProductType, RoleType, SaleType, SteeringWheelSideType, SubjectType,
TimeAbroadType, VehicleCategoryType, VehicleType, VehicleUsageType.
Komendy
createInsurerCommands(commandPrefix: string): InsurerCommands
defineCommand<TPayload>(commandName: string): CommandDefinition<TPayload>Komendy: dlaczego jawny prefiks
pp-command-bus routuje komendy po nazwie klasy: Command ustawia this.__name =
this.constructor.name, a CommandBus.handle() rejestruje handler pod kluczem
commandDefinition.name. Jeżeli nazwa klasy po stronie ICC i po stronie workera różni się choćby
jednym znakiem, komenda nigdy nie dotrze - bez błędu i bez logu.
Fabryka przyjmuje jawny prefiks w PascalCase, a nie nazwę towarzystwa do przekształcenia.
Nazwa towarzystwa w metadanych to interriskIrys, a nazwy klas to InterriskIrys*Command -
gdyby fabryka wyprowadzała prefiks sama, wynik zależałby od implementacji pascalCase
(InterriskIrys czy Interriskirys?), a pomyłka oznaczałaby cicho martwy kanał. Prefiks jest
walidowany schematem; wartość spoza PascalCase kończy się wyjątkiem przy starcie, a nie
milczeniem w runtime.
defineCommand ustawia nazwę klasy jawnie przez Object.defineProperty, więc jest odporny
na minifikację i na nazwę zmiennej, do której klasa zostanie przypisana.
Worker: rejestracja handlera
import { createInsurerCommands, proposalTaskSchema } from 'pp-iapi-contracts';
import { CommandBus, CommandBusConfig } from 'pp-command-bus';
const commands = createInsurerCommands('InterriskIrys');
const commandBus = new CommandBus(new CommandBusConfig({ redisUrl }));
commandBus.handle(commands.proposal, async (command) => {
const validation = proposalTaskSchema.safeParse(command.__payload);
if (!validation.success) {
return proposalHandler.rejectWithContractError(validation.error.issues);
}
return proposalHandler.handle(command.__payload);
});Walidację kontraktową uruchamiamy przed walidacją wymagań towarzystwa. Pierwsza odpowiada na pytanie "czy ICC przysłał to, co obiecał", druga na "czy da się z tego zbudować żądanie do API konkretnego TU". Rozdzielenie ich jest celem tej paczki.
ICC: wywołanie komendy
import { createInsurerCommands, type ProposalTask } from 'pp-iapi-contracts';
const commands = createInsurerCommands('InterriskIrys');
const task: ProposalTask = { payload: { data, metadata } };
const result = await commandBus.call(new commands.proposal(task), 120_000);Obie strony wywołują tę samą fabrykę z tym samym prefiksem, więc zgodność nazwy kanału wynika z definicji, a nie z dyscypliny przy przepisywaniu.
Wersjonowanie
Wersjonowanie semantyczne, wydania przez semantic-release (conventional commits).
| Zmiana kontraktu | Wersja | Typ commita |
|---|---|---|
| Dodanie pola opcjonalnego | minor | feat |
| Dodanie wartości do słownika | minor | feat |
| Rozszerzenie typu pola (np. dopuszczenie null) | minor | feat |
| Zmiana typu pola na węższy | major | feat! / BREAKING CHANGE |
| Zmiana pola opcjonalnego na wymagane | major | feat! / BREAKING CHANGE |
| Usunięcie pola lub wartości słownika | major | feat! / BREAKING CHANGE |
| Zmiana nazwy klasy komendy | major | feat! / BREAKING CHANGE |
Zmiana nazwy komendy jest zawsze major - rozjazd nazw zrywa kanał po cichu, więc obie strony
muszą zaktualizować paczkę w tym samym wdrożeniu.
Build to czysty tsc. Bundlery i minifikacja są zakazane: nazwy klas komend mają znaczenie
w runtime.
Testy
npm testTrzy grupy testów uruchamiane w CI:
- Nazwy komend -
new Cmd(payload).__namerówna się nazwie kontraktowej, fabryka odtwarza istniejące nazwyInterriskIrys*Command, prefiks spoza PascalCase jest odrzucany. - Golden-file - 20 zanonimizowanych payloadów realnego ruchu (po kilka na każdą z pięciu par zadanie/wynik, z różnych towarzystw) przechodzi walidację, nie zawiera pól nieznanych kontraktowi i nie zawiera koperty transportowej.
- Testy negatywne - brak pola wymaganego kończy się
success === falseze wskazaniem ścieżki błędu; wartości spoza słownika są odrzucane.
Fixtures leżą w src/__fixtures__. Dane osobowe (PESEL, imię, nazwisko, adres, numer
rejestracyjny, VIN, telefon, e-mail, REGON, NIP) są zanonimizowane, treści dokumentów i tokeny
zastąpione wartościami testowymi.
