The package wraps the Bonline (Bulgarian bank) HTTP API behind a small set of fluent entry points. Minimum steps to a first result:
composer require the package, publish the config (php artisan vendor:publish --tag=bonline-config), and fill in your credentials/account identifiers in .env.Dto namespace — the DTOs (BalanceDto, TransactionDto, TodayActivityDto, CardPaymentDto) are the real contract you code against, and their public properties tell you exactly what Bonline returns.$balance = Bonline::balance()
->account($account)
->currency('BGN')
->get(); // BalanceDto
$balance->availableBalance;
$balance->currentBalance;
$balance->statementBalance; // new in 1.1
$balance->blockedAmount; // derived when Bonline omits it
todayActivities()->get(), which returns TodayActivityDto[] and is the natural payload for a dashboard or an end-of-day job.Statement streaming into persistence. stream() is a generator over pages; in v1.1 it follows TotalCount rather than the per-page Count, so a full year now yields ~2.9k rows instead of silently truncating at 1,000. The day-to-day pattern is to pipe it straight into an upsert keyed on the transaction id, inside a queued job:
foreach (Bonline::statement()->account($account)->from($from)->to($to)->stream() as $tx) {
$rows[] = $tx->toArray();
if (count($rows) >= 500) { Transaction::upsert($rows, ['id']); $rows = []; }
}
Because it now walks every page, budget for runtime — run it on a queue with a generous timeout, and log progress by counting rows against TotalCount.
Explicit paging. When you want to resume or control batch size, statement()->page($n) now correctly unwraps the {Id, Count, TotalCount, Records} envelope and returns records; a bare list is still accepted for compatibility with any fixtures you built earlier.
Balances for dashboards. Call balance()->get() per account/currency and persist accountKey, availableBalance, currentBalance, statementBalance. Prefer blockedAmount as returned and only fall back to your own derivation if it is missing — the DTO already derives it when Bonline omits it.
Today's activities as a first-class feed. Treat TodayActivityDto as its own type, not a TransactionDto. Filter with isCredit() and read credit, debit, nomination directly; use raw() when you need a field the DTO doesn't map yet.
Card payments as an enrichment step. After ingestion, call cardPayment() on each TransactionDto/TodayActivityDto. When it returns a CardPaymentDto (including for older statements that bury the narrative in EntryComment), persist terminal, card, auth code, gross amount, commission and net amount as structured columns — commission/net are what finance actually reconciles against.
Idempotent, scheduled sync. Schedule todayActivities hourly and statements daily; wrap each in retry() with backoff, and always persist the raw record alongside the mapped fields so a DTO mapping fix doesn't force a re-fetch.
todayActivities()->get() now returns TodayActivityDto[]. If you previously read entryAmountCredit / entryAmountDebit off that feed, those values were always empty — the feed uses Credit, Debit, Nomination, PostDate, so the old mapping silently produced zeros. Switch to credit / debit / nomination and type-hint the new DTO; any code doing instanceof TransactionDto on today's feed will now fail.balance()->get() no longer appends /{0|1} to the path (Bonline answered HTTP 400) and now sends initStatementBalance as a query parameter. If you had worked around the 400 with your own HTTP call, delete it.TotalCount, not Count. This was the root cause of the truncated statement bug. If you wrote your own paging loop against this package, audit it for the same mistake; Count is only the size of the current page (max 1000).TotalCount is exact — the loop stops on an empty page as a safety net. Log when records received ≠ TotalCount; it usually means a filter or date boundary is off.page() unwraps the envelope. Don't defensively re-read ['Records'] in your code unless you're handling both shapes deliberately — the DTO layer already does.CardPaymentDto::fromText() parses terminal text and older EntryComment narratives; keep the original string and treat parsing failures as "unknown", not as an exception that aborts a batch.BalanceDto::$accountKey echoes the response, but the request context is what you should join on.raw() for one record per page when something looks wrong; the DTO is a thin mapping layer, and discrepancies are nearly always field-name mismatches in the upstream feed rather than transport errors.How can I help you explore Laravel packages today?