speedfin / speedfin-wrapper
Speedfin 2.0 API wrapper for Finance Admin (clients, documents, etc.)
Requires
- php: >=8.2
- psr/log: ^3.0
- symfony/cache: ^6.4 || ^7.4 || ^8.0
- symfony/config: ^6.4 || ^7.4 || ^8.0
- symfony/dependency-injection: ^6.4 || ^7.4 || ^8.0
- symfony/http-client: ^6.4 || ^7.4 || ^8.0
- symfony/http-kernel: ^6.4 || ^7.4 || ^8.0
- symfony/serializer: ^6.4 || ^7.4 || ^8.0
- symfony/yaml: ^6.4 || ^7.4 || ^8.0
Requires (Dev)
None
Suggests
None
Provides
None
Conflicts
None
Replaces
None
README
Klient PHP do API Speedfin Backend. Pakiet udostępnia moduły AdviserProfile, Calendar, Notification, Client, ProductPrint i SchedulePrint.
Wymagania
- PHP 8.2 lub nowszy.
- Symfony
^6.4 || ^7.4 || ^8.0. Symfony 8 wymaga PHP 8.4 lub nowszego.
Obsługa klientów w Symfony
Dodaj pakiet speedfin/speedfin-wrapper do zależności aplikacji. Zarejestruj bundle w config/bundles.php.
SpeedfinWrapper\SpeedfinWrapperBundle::class => ['all' => true],
W config/packages/speedfin_wrapper.yaml skonfiguruj adres bazowy Backendu. W apps/ai można użyć zmiennej SPEEDFIN_BASE_URL.
speedfin_wrapper:
api_domain: '%env(resolve:SPEEDFIN_BASE_URL)%'
Wstrzyknij ClientServiceInterface do serwisu aplikacji. W metodzie obsługującej zalogowanego doradcę przekaż jego aktualny token dostępu w postaci samego JWT, bez prefiksu Bearer.
use SpeedfinWrapper\Client\Dto\ClientCreateDto;
use SpeedfinWrapper\Client\Service\ClientServiceInterface;
final readonly class CreateAdviserClient
{
public function __construct(private ClientServiceInterface $clients)
{
}
public function create(string $adviserToken): int
{
return $this->clients->create(
new ClientCreateDto(name: 'Jan', surname: 'Kowalski', email: 'jan@example.com'),
$adviserToken,
)->id;
}
}
create() wysyła POST /api/clients jako application/ld+json. name i surname są wymagane. cellPhone, email i comment są opcjonalne. Backend przypisuje klienta do doradcy z tokenu. Wrapper zwraca ClientCreatedDto po odpowiedzi 201 Created z id w postaci dodatniej liczby całkowitej.
Ten sam serwis udostępnia listę, szczegóły i częściową edycję danych klienta. Każde wywołanie przyjmuje aktualny token doradcy.
use SpeedfinWrapper\Client\Dto\ClientUpdateDto;
$list = $clients->getAll($adviserToken);
$filtered = $clients->getAll($adviserToken, page: 1, limit: 20, search: 'Jan Kowalski');
$client = $clients->getById($clientId, $adviserToken);
$updated = $clients->update($clientId, new ClientUpdateDto(email: 'nowy@example.com'), $adviserToken);
getAll() zwraca tablicę ClientItemDto, a getById() i update() zwracają jeden ClientItemDto. DTO zawiera id, name, surname, cellPhone, email i comment. update() wysyła PATCH w formacie application/merge-patch+json. Pola pominięte w ClientUpdateDto zachowują dotychczasowe wartości. Aby wyczyścić cellPhone, email lub comment, przekaż pusty ciąg. Odpowiedź 404 przy odczycie lub edycji jest dostępna jako ClientExceptionInterface.
Opcjonalne page, limit i search są przekazywane jako parametry query do GET /api/clients. Wartości null są pomijane. search przyjmuje jeden tekst o długości do 255 znaków. Backend wyszukuje fragment imienia, nazwiska, e-maila lub pełnego imienia i nazwiska w obu kolejnościach. Normalizuje białe znaki i ignoruje wielkość liter. Pusty tekst lub same białe znaki oznaczają zwykłe listowanie. Przekaż tekst wprost, a Symfony HttpClient zakoduje znaki specjalne i Unicode. Backend traktuje % i _ dosłownie i zwraca błędy walidacji API przy niepoprawnych parametrach.
Błędy walidacji i autoryzacji są dostępne jako ClientExceptionInterface z Symfony HttpClient. Kod HTTP można odczytać przez $exception->getResponse()->getStatusCode(), a treść JSON przez $exception->getResponse()->toArray(false). Po błędzie transportu wywołujący decyduje o ponowieniu zapisu, ponieważ Backend mógł już utworzyć klienta.
Wydruki hipoteczne i trwałe PDF
ProductPrintServiceInterface korzysta z tego samego api_domain i rejestracji bundle co moduł Client. W Symfony 8 wstrzyknij interfejs do serwisu aplikacji. Przekaż aktualny token doradcy i referencje snapshotów z istniejących wyników kalkulatora.
use SpeedfinWrapper\ProductPrint\Dto\ProductPrintCreateDto;
use SpeedfinWrapper\ProductPrint\Dto\ProductPrintFilterDto;
use SpeedfinWrapper\ProductPrint\Dto\ProductPrintItemDto;
use SpeedfinWrapper\ProductPrint\Dto\ProductPrintPdfDto;
use SpeedfinWrapper\ProductPrint\Dto\ScenarioContextDto;
use SpeedfinWrapper\ProductPrint\Service\ProductPrintServiceInterface;
final readonly class DownloadMortgageOffers
{
public function __construct(private ProductPrintServiceInterface $prints)
{
}
public function download(
#[\SensitiveParameter] string $adviserToken,
string $sessionId,
int $offerId,
string $snapshotRequestId,
): ProductPrintPdfDto {
$scenario = new ScenarioContextDto($sessionId);
$definition = $this->prints->createPrint(
new ProductPrintCreateDto(
filter: new ProductPrintFilterDto(creditValue: 400000, secureValue: 600000, creditPeriod: 300),
items: [new ProductPrintItemDto(id: $offerId, requestId: $snapshotRequestId, sort: 0, print: [])],
print: [],
),
$adviserToken,
$scenario,
);
$artifact = $this->prints->createArtifact($definition, $adviserToken);
$receipt = $this->prints->getArtifactReceipt($artifact->id, $adviserToken, $artifact->scenario);
return $this->prints->downloadArtifact($receipt, $adviserToken);
}
}
createPrint() wysyła POST /api/products/prints z Accept i Content-Type równymi application/ld+json. Odpowiedź 201 jest mapowana na ProductPrintCreatedDto z slug, status i kontekstem scenariusza. createArtifact() przyjmuje ten wynik, dzięki czemu używa kontekstu z utworzenia definicji. Wysyła POST /api/products/prints/{slug}/artifacts z Accept: application/json i zwraca ProductPrintArtifactReceiptDto z id, filename i scenario po odpowiedzi 201.
getArtifactReceipt() wykonuje GET /api/products/print-artifacts/{id}/receipt z Accept: application/json. Weryfikuje odpowiedź 200, format UUID v4, bezpieczną nazwę pliku PDF oraz zgodność ID z żądaniem. downloadArtifact() przyjmuje potwierdzenie i wykonuje GET /api/products/print-artifacts/{id} z Accept: application/pdf. Zwraca ProductPrintPdfDto z id, filename i zawartością content przechowywaną w pamięci. Sprawdza typ odpowiedzi i sygnaturę %PDF-. Domyślny limit to 20_000_000 bajtów, a trzeci argument maxBytes pozwala ustawić własny limit. Limit jest sprawdzany podczas odczytu strumienia.
Ścieżka trwałych artefaktów jest przeznaczona dla hipotek. ProductPrintCreateDto wysyła type: mortgage. Filtr obsługuje investmentValue, creditValue, secureValue, incomeValue, creditPeriod, creditClientAge, ability, bankNameVisible i currencyIndex. ability przyjmuje tablicę zgodną z kontraktem Backendu, z gospodarstwami i wnioskodawcami. Kolejność pozycji, ich sort, istniejące requestId oraz tablice opcji print są przekazywane do Backendu w podanej postaci.
Opcjonalny ScenarioContextDto tworzy nagłówek X-Speedfin-Scenario-Context z schema: 1 i UUID sesji. Każda operacja przyjmuje jawny token doradcy. Transport bundle używa osobnego NativeHttpClient, wyłącza przekierowania i wykonuje pojedyncze żądanie dla każdej operacji.
Wyjątki dziedziczą po ProductPrintException. Udostępniają operation, statusCode i isWriteResultUncertain().
ProductPrintRejectedExceptionoznacza jawne odrzucenie HTTP 4xx, w tym406przy negocjacji formatu definicji.ProductPrintUnconfirmedExceptionoznacza błąd transportu lub niepotwierdzony wynik, w tym HTTP408i5xx.ProductPrintInvalidResponseExceptionoznacza błędny JSON, niepoprawne pola odpowiedzi, niepoprawny PDF lub przekroczenie limitu rozmiaru.
Po błędzie operacji zapisu sprawdź isWriteResultUncertain() przed decyzją o ponowieniu. Wartość true oznacza, że zapis mógł już nastąpić, również gdy odpowiedź 201 miała niepoprawną treść. Etap odczytasz z operation->value, a kod HTTP z statusCode. Przy błędach argumentów przed wysłaniem żądania zgłaszany jest InvalidArgumentException.
Harmonogram spłaty kredytu hipotecznego
Wstrzyknij SchedulePrintServiceInterface. Serwis korzysta z speedfin_wrapper.api_domain i w każdej operacji przyjmuje aktualny token doradcy w postaci samego JWT. Opcjonalny ScenarioContextDto z utworzenia definicji jest przekazywany automatycznie przy pobraniu.
use SpeedfinWrapper\ProductPrint\Dto\ScenarioContextDto;
use SpeedfinWrapper\SchedulePrint\Dto\SchedulePrintCreateDto;
use SpeedfinWrapper\SchedulePrint\Dto\SchedulePrintInterestPeriodDto;
use SpeedfinWrapper\SchedulePrint\Dto\SchedulePrintPdfDto;
use SpeedfinWrapper\SchedulePrint\Enum\SchedulePrintType;
use SpeedfinWrapper\SchedulePrint\Service\SchedulePrintServiceInterface;
final readonly class DownloadMortgageSchedule
{
public function __construct(private SchedulePrintServiceInterface $prints)
{
}
public function download(
#[\SensitiveParameter] string $adviserToken,
?ScenarioContextDto $scenario = null,
): SchedulePrintPdfDto {
$definition = $this->prints->createPrint(
new SchedulePrintCreateDto(
creditValue: 200_000,
creditPeriod: 180,
margins: [new SchedulePrintInterestPeriodDto(periodFrom: 1, periodTo: 180, interestRate: 5)],
type: SchedulePrintType::EqualInstallmentFull,
),
$adviserToken,
$scenario,
);
return $this->prints->downloadPdf($definition, $adviserToken);
}
}
Przykład generuje pełny harmonogram rat równych dla 200 000 PLN na 180 miesięcy przy nominalnym oprocentowaniu 5% rocznie przez cały okres. EqualInstallmentFull określa rodzaj rat i odpowiada FIXED_INSTALLMENT_FULL = 1 w Backendzie. interestRate w margins[] jest roczną stopą procentową, a periodFrom i periodTo oznaczają miesiące włącznie. Okresy muszą kolejno pokrywać cały czas spłaty. Backend oblicza harmonogram i renderuje PDF. DTO żądania obejmuje kwotę, czas spłaty, okresy oprocentowania i wariant wydruku. Konfiguracja transz pozostaje poza tym zakresem.
createPrint() wysyła POST /api/scheduler/prints jako application/ld+json. Po 201 Created zwraca SchedulePrintCreatedDto z slug, status równym CREATED i kontekstem. downloadPdf() wykonuje GET /api/scheduler/prints/{slug}. Backend renderuje PDF przy każdym takim żądaniu. SchedulePrintPdfDto zawiera filename i bajty content. Aplikacja może zachować te bajty we własnej pamięci trwałej i użyć ich ponownie, np. do kilku wysyłek z tym samym załącznikiem.
Zasób GET w Backendzie deklaruje obecnie application/vnd.openxmlformats jako jedyny typ negocjacji dla formatu pdf, a udane pobranie zwraca Content-Type: application/pdf. Wrapper wysyła Accept: application/pdf, application/vnd.openxmlformats;q=0.9, żeby obsłużyć obecną deklarację oraz jej korektę. Po stronie Backendu należy zarejestrować application/pdf w formats['pdf'] operacji GET. Można tymczasowo zachować dotychczasowy typ jako alias dla starszych klientów.
Wrapper weryfikuje MIME, nazwę pliku z Content-Disposition i sygnaturę %PDF-. Limit 20 MB (20_000_000 bajtów) jest sprawdzany podczas odczytu. Transport wyłącza przekierowania i automatyczne ponawianie zapisu. Wyjątki SchedulePrintRejectedException, SchedulePrintUnconfirmedException i SchedulePrintInvalidResponseException dziedziczą po SchedulePrintException i udostępniają operation, statusCode oraz isWriteResultUncertain(). statusCode jest null, jeśli kod HTTP nie był dostępny. Niepewny wynik zapisu oznacza, że Backend mógł już zapisać definicję.
Trwały artefakt harmonogramu
Obecny slug identyfikuje definicję harmonogramu. Backend potrzebuje osobnego kontraktu, żeby apps/ai mogło później pobierać ten sam PDF bez renderowania i bez przechowywania bajtów po swojej stronie. Proponowane operacje odpowiadają strukturze API ProductPrint.
| Proponowana operacja Backendu | Odpowiedź i działanie |
|---|---|
POST /api/scheduler/prints/{slug}/artifacts | Renderuje i zapisuje bajty, a po zapisie zwraca 201 z potwierdzeniem JSON zawierającym odrębne id (UUID), filename i contentType. |
GET /api/scheduler/print-artifacts/{id}/receipt | Zwraca 200 z tym samym potwierdzeniem JSON. |
GET /api/scheduler/print-artifacts/{id} | Zwraca 200, Content-Type: application/pdf, Content-Disposition i zapisane bajty, bez ponownego renderowania. |
Każda z proponowanych operacji powinna sprawdzać tożsamość doradcy i zgodność kontekstu scenariusza. Wrapper udostępnia dziś createPrint() i downloadPdf().
Kalendarium CRM
Wstrzyknij CalendarServiceInterface. Serwis korzysta z speedfin_wrapper.api_domain. Każde wywołanie przyjmuje aktualny JWT doradcy jako $adviserToken. Wrapper przesyła daty z offsetem i dokładnością do sekund oraz identyfikator strefy IANA. Backend sprawdza uprawnienia, interpretuje czas lokalny i oblicza wystąpienia oraz terminy przypomnień.
| Metoda | Operacja względem /api/crm/calendar | Wynik |
|---|---|---|
createEvent() | POST /events | 201, CalendarEventDto |
getEvent() | GET /events/{id} | 200, CalendarEventDto |
updateEvent() | PATCH /events/{id} | 200, CalendarEventDto |
cancelEvent() | POST /events/{id}/cancel | 200, CalendarEventDto ze stanem canceled |
listOccurrences() | GET /occurrences | 200, CalendarOccurrencePageDto |
moveOccurrence() | PATCH /events/{id}/occurrences/{occurrenceKey} | 200, definicja serii z nową wersją |
cancelOccurrence() | POST /events/{id}/occurrences/{occurrenceKey}/cancel | 200, definicja serii z nową wersją |
getOccurrenceReminders() | GET /events/{id}/occurrences/{occurrenceKey}/reminders | 200, CalendarOccurrenceRemindersDto |
Spotkanie z klientem i przypomnieniem
Przykład zakłada istniejącego klienta doradcy i cotygodniowe spotkanie w czwartki. Dwie reguły przekazują przypomnienie doradcy w aplikacji oraz klientowi przez e-mail.
use SpeedfinWrapper\Calendar\Dto\CalendarEventCreateDto;
use SpeedfinWrapper\Calendar\Dto\CalendarRecurrenceDto;
use SpeedfinWrapper\Calendar\Dto\CalendarReminderRuleDto;
use SpeedfinWrapper\Calendar\Enum\CalendarFrequency;
use SpeedfinWrapper\Calendar\Enum\CalendarReminderChannel;
use SpeedfinWrapper\Calendar\Enum\CalendarReminderMode;
use SpeedfinWrapper\Calendar\Enum\CalendarReminderRecipient;
use SpeedfinWrapper\Calendar\Service\CalendarServiceInterface;
/** @var CalendarServiceInterface $calendar */
$series = $calendar->createEvent(
new CalendarEventCreateDto(
type: 'MEETING',
title: 'Omówienie dokumentów',
startsAt: new \DateTimeImmutable('2026-11-19T15:00:00+01:00'),
endsAt: new \DateTimeImmutable('2026-11-19T16:00:00+01:00'),
timezone: 'Europe/Warsaw',
clientId: $clientId,
recurrence: new CalendarRecurrenceDto(CalendarFrequency::Weekly, interval: 1, weekdays: [4], count: 4),
reminders: [
new CalendarReminderRuleDto(
CalendarReminderMode::AtLocalTime,
CalendarReminderRecipient::Adviser,
CalendarReminderChannel::InApp,
localTime: '10:00',
dayOffset: 0,
),
new CalendarReminderRuleDto(
CalendarReminderMode::BeforeStart,
CalendarReminderRecipient::Client,
CalendarReminderChannel::Email,
minutesBefore: 60,
),
],
),
$adviserToken,
idempotencyKey: $creationKey,
);
Przy tworzeniu przekaż klucz przypisany do operacji użytkownika. Backend dopuszcza jego pominięcie. Powtórzenie tego samego żądania z tym samym kluczem zwraca aktualną reprezentację wydarzenia. Zmiana danych przy użyciu tego samego klucza daje konflikt. Pola opcjonalne o wartości null w DTO tworzenia są pomijane.
Listowanie, przesunięcie terminu i odwołanie serii
use SpeedfinWrapper\Calendar\Dto\CalendarOccurrenceMoveDto;
use SpeedfinWrapper\Calendar\Dto\CalendarOccurrenceQueryDto;
$page = $calendar->listOccurrences(
new CalendarOccurrenceQueryDto(
from: new \DateTimeImmutable('2026-11-26T00:00:00+01:00'),
to: new \DateTimeImmutable('2026-11-27T00:00:00+01:00'),
clientId: $clientId,
page: 1,
itemsPerPage: 25,
),
$adviserToken,
);
// $occurrence to element $page->items wybrany przez doradcę.
$moved = $calendar->moveOccurrence(
$occurrence->eventId,
$occurrence->occurrenceKey,
new CalendarOccurrenceMoveDto(
expectedVersion: $occurrence->version,
startsAt: new \DateTimeImmutable('2026-11-26T16:00:00+01:00'),
endsAt: new \DateTimeImmutable('2026-11-26T17:00:00+01:00'),
),
$adviserToken,
);
$canceled = $calendar->cancelEvent($moved->id, $moved->version, $adviserToken);
Wynik listowania zawiera items, totalItems, odnośniki stronicowania w view i pozostałe metadane JSON-LD w metadata. Do kolejnego zapytania przekaż zakres i numer strony. Po przesunięciu używaj oryginalnego occurrenceKey. Każda edycja i każde odwołanie wymagają wersji z ostatniego potwierdzonego odczytu lub zapisu.
W CalendarEventUpdateDto pomiń clientId, description lub recurrence, aby zachować dotychczasową wartość. Podaj jawne null, aby ją wyczyścić. reminders: [] usuwa wszystkie reguły. Aktualizacja istniejącej reguły wymaga przekazania jej id i pełnej definicji.
Odczyt dostaw zachowuje status i reason Backendu. blocked oznacza blokadę dostawy. accepted oznacza przyjęcie przez kanał. isOutcomeUnknown() rozpoznaje zarówno zwracane przez Backend uncertain, jak i outcome_unknown. Oba stany wymagają uzgodnienia wyniku z dostawcą przed ponowieniem dostawy.
Wrapper negocjuje JSON-LD, wysyła POST jako JSON i PATCH jako JSON Merge Patch. Wyłącza przekierowania i automatyczne ponawianie. CalendarException udostępnia operation, reason i statusCode. reason rozróżnia walidację, autoryzację, brak zasobu, konflikt idempotencji, konflikt wersji lub stanu wydarzenia, pozostałe odrzucenia, uszkodzoną odpowiedź, niedostępny odczyt i niepotwierdzony zapis. Jeśli isWriteResultUncertain() zwraca true, Backend mógł zapisać zmianę. Przed kolejną edycją odczytaj bieżącą wersję. Przy ponowieniu utworzenia użyj tego samego klucza i tych samych danych.
Kolejne wydanie
Moduł Calendar jest przewidziany w wydaniu 1.0.10. Historia zmian znajduje się w CHANGELOG.md.