EPaySe

changelog.title

v2.1.0

changelog.subtitle

changelog.types.feature
changelog.categories.payment

Protest fees now split into provider and internal, priced from the contract

The excessive-chargeback (protest) fee a merchant pays is now defined on their contract as progressive tiers, and each charge is split into the acquirer's portion and EPaySe's own share, with EPaySe's share being whatever remains after the provider's cut — which may be negative, and is left so rather than hidden. The tiers are read as they stood at the time of the original transaction, not as they stand today, so a dispute is priced against the terms that were in force. Contracts that have not configured protest tiers keep charging exactly as before, so nothing changes for existing merchants until an admin sets them up. A missing provider cost no longer aborts the charge; the whole fee simply falls to EPaySe. The margin scan that watches the other fee types for a negative EPaySe margin now watches protest too, so a contract tier priced below the provider's is surfaced rather than quietly absorbed. Admins configure the tiers on the contract form, where the fee amounts are capped to two decimals so the split always reconciles to the total, tiers must be contiguous, and a warning is raised — without blocking the save — when the contract tiers do not line up with the provider's.

changelog.types.feature
changelog.categories.payment

A merchant's contract can waive the fee on failed attempts

The fixed processing fee charged on a failed payment attempt can now be turned off per merchant through their contract. It stays on by default, so nothing changes for merchants who have not negotiated otherwise and for those with no contract at all. When it is turned off the fee is not booked to the ledger, and the fee snapshot recorded against the attempt is zeroed to match — so the two records of what was charged never disagree for that attempt. The toggle lives on the contract form; the flag it sets is validated and persisted on both creating and editing a contract, and each change is recorded to the merchant's activity log with its old and new value, because turning the fee off is a change to revenue and the contract table keeps no history of its own.

changelog.types.fix
changelog.categories.dashboard

The negative-balance warning now actually reaches admins

The warning that tells an admin a refund will overdraw a merchant existed but did not reach them. Started from the transactions list it never appeared at all, because the merchant id it needs was not passed through — it only worked from a transaction's detail page. When the balance lookup failed it treated the merchant as solvent and stayed quiet, and that lookup fails for admins who can issue refunds but cannot view fund flows, since those are separate permissions. And it compared the balance against the transaction's full refundable amount rather than the amount being refunded, so it also fired on refunds the merchant could comfortably afford. The confirmation now shows the balance the merchant is left with rather than an abstract deficit, says so plainly when the balance could not be read, and appears on both paths. Approving a refund that was held for balance also re-checks it — the figure that held it may be days old — and raises an alert if the approval really does take the account negative.

changelog.types.feature
changelog.categories.payment

Refunds held for balance now raise an alert

A refund held because the merchant's available balance is short now raises a Telegram alert carrying the shortfall, so someone can decide whether to approve it or wait. Nothing moves a refund out of that state except an admin opening the queue — there is no sweep that retries once the merchant is funded again — so without the alert a held refund could sit indefinitely while the buyer waited. The alert is sent after the transaction commits, because it posts over HTTP and an open money transaction is no place for a network call. The balance check itself is unchanged and still uses available funds only: reserve exists to cover refunds and chargebacks, so spending it to fund a refund would defeat its purpose. It now reads that figure directly instead of computing five balances to use one of them.

changelog.types.breaking
changelog.categories.settlement

Settlement moves to T+7 on every tier

Settlement now runs T+7 on every tier. Enterprise was on T+0 and the rest on T+1; the distinction between tiers is price, not payout speed, and 7 days is the standard term. Settlements already in flight are unaffected — the release date is written onto the transaction and its ledger entry when the payment is booked, and the release sweep reads those stored values, so nobody waits longer for money already on its way. Historical rate snapshots are left alone, as reversals must price against the terms in force at the time of the original transaction.

changelog.types.fix
changelog.categories.settlement

Rolling reserve moves to 180 days

Rolling reserve now runs 180 days instead of 90, the common term for high-risk processing. Reserves already being held are untouched: the release date is written as a concrete date when the reserve is created and nothing recalculates it, so no merchant's existing hold is extended. Historical rate snapshots are also left alone — refunds and disputes are priced against the terms in force when the original transaction was taken, and rewriting those rows would price past reversals against terms that never existed. Separately, a reserve created without a resolved fee configuration used to fall back to a 30-day term that matched nothing else in the system, quietly releasing funds two months early; it now uses the standard term and records a warning, because that path only runs when fee resolution has already gone wrong.

changelog.types.fix
changelog.categories.payment

A missing exchange rate now pages someone

When an exchange rate cannot be resolved, the payment is still booked to the ledger — that was fixed earlier — but the resulting gap needed a person, not a log line. The transaction is left without settlement currency, rate, or amount, and somebody has to backfill them by hand. The failure now raises a Telegram alert carrying the transaction id, the presentment currency and the amount paid, so operations learn about it while it is still one transaction rather than a reconciliation surprise. The alert call is itself wrapped, so a messaging outage can never undo the booking it is reporting on.

changelog.types.fix
changelog.categories.payment

Fee rounding no longer creates money out of nothing

Every fee component used to be truncated to two decimals independently, so on any payment whose amount was not a round figure the ledger's immediate entries no longer summed to zero — net plus fees fell one to three cents short of the amount paid, and the gap was credited to the merchant's available balance as funds EPaySe had never received. Round amounts such as 100.00 produced no gap, which is why the defect stayed invisible to the existing tests. Third-party fees and the merchant's reserve are now rounded on their own merits, the merchant's net is floored so rounding can never favour it, and EPaySe's own fee carries the remainder so the accounting identity closes exactly. Fund flow entries below one cent are also no longer discarded: only an exact zero is skipped, as the database constraint requires.

changelog.types.fix
changelog.categories.payment

A missing exchange rate can no longer skip the ledger

A payment the PSP had already collected could end up with no ledger entry at all. Currency conversion ran before the fund flows were written, and it throws when no exchange rate can be resolved — so a missing rate skipped the booking entirely. On the hosted confirmation path the exception was then swallowed while the surrounding transaction committed, leaving the attempt marked successful, the webhook token spent, and the ledger empty; because the token was gone, the provider's retry was rejected as a duplicate and the payment could not recover. The ledger is now written first and conversion failures are caught and logged, since settlement figures are reporting metadata that can be backfilled while a missing ledger entry cannot. The confirmation path no longer swallows failures, restoring the roll-back-everything behaviour its surrounding transaction was written to provide.

changelog.types.fix
changelog.categories.platform

Exchange-rate freshness monitor no longer false-alarms on daily providers

The scheduled exchange-rate freshness check now measures the time since the last successful upstream FETCH instead of the last rate VALUE change. Because our primary provider publishes once per day, unchanged values were being misread as "stale" for hours, firing a warning, a Cron Monitor failure, and an unhandled-exception alert every 15 minutes. The command is now a self-contained monitor that emits a single cooldown-guarded alert only on a genuine fetch outage and exits cleanly otherwise.

changelog.types.feature
changelog.categories.payment

Per-merchant contract fees

Merchants can now be placed on a negotiated contract fee — copied from a tier template then adjusted per merchant — that overrides tier pricing. EPaySe's internal fee is derived at runtime (contract minus provider minus affiliate) and may be negative for deliberate loss-leader deals; admin margin reports surface this by design. Contract fees are versioned so historical refunds and disputes always price against the contract in effect at the time of the original transaction.

changelog.types.deprecated
changelog.categories.payment

Fee-tier admin management hidden in favor of contract fees

The standalone fee-tier admin screens and per-merchant tier assignment are now hidden behind a configuration flag (off by default), as per-merchant contract fees supersede them operationally. Fee tiers still function behind the scenes — contracts fall back to tier rates for any currency they do not cover, and a tier is still used as the starting template when creating a contract — so existing pricing is unaffected. The screens can be re-enabled from configuration if needed.

changelog.types.fix
changelog.categories.fraud

Stop fraud velocity checks from blocking local end-to-end test transactions

The IP/email/phone velocity and cross-match fraud checks count all traffic by source. In local development and end-to-end tests every request originates from a single test-runner address at high volume, which legitimately tripped these checks and blocked essentially every test transaction with a high risk score. These volume checks are now skipped in the local environment only (configurable), while production, staging and the automated test suite keep full fraud coverage.

changelog.types.improvement
changelog.categories.settlement

Speed up admin dashboard system-balance under concurrent load

The system-balance calculation checked its cache only after acquiring a single-flight lock, so every dashboard load waited for the lock even when the value was already cached — several admins opening the dashboard at once could each wait up to five seconds. The cache is now read before the lock, so a warm cache serves instantly and only a genuine cache miss contends for the lock; the direct-compute fallback also warms the cache so later readers stay on the fast path.

changelog.types.fix
changelog.categories.dashboard

Fix 500 error on webhook and payment-account pages when access is denied

When a merchant user without the right permission opened the Webhooks list, a webhook's events, the Payment Accounts list, or a payment account's detail, the page returned a 500 error instead of a clean 'not authorized' page. The controllers declared an Inertia-only return type, but the authorization helper returns a plain HTTP response to stamp the 403/404 status, causing a type error. The affected methods now accept both response types, so denied access shows the proper 403 (or 404 for a specific record) as intended.

changelog.types.fix
changelog.categories.settlement

Fix admin dashboard 500 under load from an uncaught balance lock timeout

The system-balance calculation guards a single-flight cache lock and is meant to fall back to a direct computation when the lock is contended. A missing import made the catch clause reference a non-existent class, so the fallback never triggered: under concurrent load (e.g. several admins opening the dashboard at once) the lock timeout propagated as an unhandled error and returned a 500 on the admin dashboard. The timeout is now caught correctly and the balance is computed directly instead.

changelog.types.improvement
changelog.categories.platform

True zero-downtime deploys: graceful QUIT shutdown + deploy-window probe gate

Deploys no longer drop in-flight requests: nginx and PHP-FPM now shut down gracefully (QUIT) with budgets sized to let running requests and PSP webhooks finish, background workers stop cleanly at shutdown start, and production queue workers receive signals correctly (previously hard-killed mid-job on every deploy). Every deploy is now measured by an in-pipeline probe that can block the pipeline when any real request fails during cutover.

changelog.types.fix
changelog.categories.dashboard

Clearer settings labels and admin infrastructure section naming

The merchant settings menu now shows a short plain-language description under technical integration items (API Keys, Webhooks, IP Whitelist, API Logs) in both English and Vietnamese, with the full text as a hover tooltip, so non-technical merchants understand what each is for while the industry-standard names stay intact. Separately, the admin 'Ops Hub' section is renamed to 'Infrastructure Tools' to match its actual contents (Horizon, log viewer, server monitoring) instead of implying an operations work-queue.

changelog.types.fix
changelog.categories.dashboard

Highlight the correct sidebar item on merchant create pages

On merchant create pages such as New Withdrawal or New Invoice, no sidebar menu item was highlighted, so it was unclear which section you were in. Create pages now highlight their parent list item, matching how detail and edit pages already behave and matching the admin sidebar.

changelog.types.fix
changelog.categories.payment

Fix payment-link create form offering unsupported currencies and leaking raw error text

The create-payment-link dialog listed currencies from the merchant's fee-tier configuration, which could include currencies the merchant cannot actually accept, so the form defaulted to a currency the server then rejected on the first submit. The dialog now offers only the currencies the merchant's payment methods support — the same list the server validates against. Separately, the payment-link validation messages referenced translation keys that did not exist, so a rejected amount, currency, or expiry date showed a raw key like 'validation.payment_link.currency_invalid'; those messages are now translated in English and Vietnamese.

changelog.types.fix
changelog.categories.dashboard

Fix admin merchant settings not saving for merchants without an email

On the admin merchant settings page, the email field was marked required in both the form and server validation, even though a merchant can legitimately have no email. For such a merchant the browser silently blocked the Save button (native required validation), so an admin could not toggle payment links, invoices, server-to-server, or fee tier at all. Email is now optional: feature toggles save regardless of email, a valid email is still accepted and saved, and an invalid email is still rejected.

changelog.types.fix
changelog.categories.platform

Fix admins unable to create or manage PSPs (missing permission)

Creating, updating, or managing payment service providers in the admin portal checked a 'manage:admin.psps' permission that was never defined in the permission config, so it was never seeded. The authorization check then failed for every administrator — including super admins — returning a 403 and blocking PSP management entirely. The permission is now defined and generated on the admin guard, and super admins receive it automatically.

changelog.types.fix
changelog.categories.settlement

Fix 500 error on settlement statistics when no date filter is applied

The merchant settlement statistics endpoint read validated `from_date`/`to_date`/`currency` keys that Laravel omits from the validated array when the request has no such query parameters, so loading the settlements page without a date filter threw an undefined-array-key error and returned a 500. The endpoint now treats absent optional filters as empty, returning statistics normally while still rejecting an invalid date.

changelog.types.fix
changelog.categories.settlement

Fix merchant balance inflating by the amount of unreleased gated credits

The running-balance ledger let a newly finalised entry chain onto an admin-gated reserve or settlement release that was still pending — pulling that not-yet-released credit into the balance. So whenever a merchant had a gated release awaiting admin confirmation and any new ledger entry was written (a payment, refund, dispute, adjustment) or the nightly balance recompute ran, the available balance would jump by the unreleased amount. Both the live write path and the recompute now skip pending gated releases, so a gated credit only enters the balance once an admin actually confirms it.

changelog.types.fix
changelog.categories.platform

Prevent the dashboard analytics cache warmer from running on every server at once

The scheduled task that pre-warms dashboard analytics caches every few minutes was missing the guard that pins a task to a single server, so on a multi-instance deployment every server would run it simultaneously — duplicating heavy aggregate queries and risking lock contention. It now runs on one server only, matching every other scheduled task.

changelog.types.feature
changelog.categories.security

Let admins enable app-based two-factor authentication

Administrators can now turn on authenticator-app (TOTP) two-factor authentication for their own account from Account Security, scan a QR code, confirm with a 6-digit code, and save one-time recovery codes. Two-factor is opt-in and not activated until confirmed. Enabling, disabling, or regenerating recovery codes each require re-entering the current password, so a hijacked session cannot silently weaken the second factor. Once enabled, the existing admin login challenge prompts for the code on the next sign-in.

changelog.types.feature
changelog.categories.security

Add password reset for the admin portal

Administrators can now recover access to the operate portal without a super-admin resetting them manually. A "Forgot password?" link on the admin login page requests a reset email, and the emailed link opens an admin-scoped reset page. The flow uses the admin guard and its own reset-token store, isolated from merchant accounts: a merchant reset link can never change an admin password even when the email matches, and the endpoint does not reveal whether an email belongs to an admin. Reset requests are rate-limited and CAPTCHA-protected like admin login.

changelog.types.fix
changelog.categories.settlement

Fix balance divergence check crashing every night in staging/production

A previous fix suspended debug tracing during the nightly balance divergence sweep by calling into a package that is only installed in local development, crashing the job with a class-not-found error on every environment where it matters. The call is now guarded to a no-op when that package isn't present, matching how the rest of the app already handles it being optional.

changelog.types.fix
changelog.categories.platform

Stop the documentation catch-all page from crashing on malformed paths

A request with an encoded double slash could reach the docs catch-all route without a page parameter bound, crashing with an unhandled error instead of returning a normal 404. The route now degrades gracefully for any unmatched path.

changelog.types.fix
changelog.categories.settlement

Fix out-of-memory crash in the nightly balance divergence check

The nightly job that independently recomputes merchant balances to detect drift was being killed by the OS for using too much memory partway through, silently skipping the night's check. Query/model activity tracing is now suspended for the duration of the job so it can stream through every merchant's ledger without accumulating memory.

changelog.types.fix
changelog.categories.marketing

Fix landing page header overflowing horizontally at md/lg breakpoints

The desktop nav and login/register buttons switched on at 768px but needed ~1240px with the Vietnamese labels, wrapping and pushing the header past the viewport width on every page using the landing layout. Split the nav into 4 primary links plus a "More" dropdown that switches on at 1024px instead. Also fixed a JSON nesting bug that showed raw i18n keys on the hero stats, moved the blog reading-time label into the eagerly-loaded common namespace so it renders correctly when embedded on the landing page, and corrected missing Vietnamese diacritics in the payment methods marquee.

changelog.types.fix
changelog.categories.dashboard

Fix admin dashboard 504 timeout

Cache heavy platform-wide analytics with single-flight locking, remove dead deferred queries, lazy-load analytics cards on scroll, and pre-warm the cache on a schedule.

changelog.types.fix
changelog.categories.dashboard

Respect feature entitlement in the account menu and fix a dead Card Rules link

The top-right account menu did not honour per-merchant feature rollout the way the sidebar does: it always showed the Transaction Settings entries (Retry Limits, Expiration Rules) even for merchants without the transaction-config feature. The submenu is now hidden until at least one of its items is available. Its Card Rules link also pointed at a route that no longer exists and raised an error when the menu was opened; it now links to the correct payment-card page and only appears when the fraud feature is enabled and the user may view payment cards. The Team Management entry, previously hidden from everyone because of a broken permission check, now displays for users who can manage the team.

changelog.types.fix
changelog.categories.settlement

Lock the running-balance chain head on successful payments

Concurrent successful-payment processing for the same merchant and currency (a real webhook racing the recovery inquiry, or manual/sandbox paths) could read the same stale balance and fork the running-balance ledger, making the available balance silently wrong. The attempt-success path now takes a row lock on the balance chain head before computing balances, matching the other money paths.

changelog.types.feature
changelog.categories.settlement

Sweep and re-inquire refunds stuck awaiting a PSP webhook

When a refund is sent to a PSP the merchant balance is held until the PSP confirms via webhook. If that webhook never arrived, the hold previously froze the balance forever. An hourly sweep now detects refunds stuck in this state, re-inquires the PSP for the real status, and either finalises them or raises an alert for manual review — so a lost webhook can no longer freeze funds indefinitely.

changelog.types.fix
changelog.categories.settlement

Make the balance-divergence sweep memory-safe on large merchants

The nightly balance:check-divergence command loaded a merchant's entire fund-flow history into memory at once, which exhausted memory (and failed the scheduled job) on high-volume merchants in production-scale data. The recompute now streams the ledger with a database cursor (constant memory), and each merchant chain is checked in isolation so a single malformed chain is logged and skipped instead of aborting the whole sweep.

changelog.types.fix
changelog.categories.settlement

Lock the PSP-freeze reserve when releasing or voiding it

PSP-freeze reserve release/void validated the can-release and remaining-amount against an in-memory model that could be stale, so two admins (or a retried request) acting on the same reserve could each create a release fund flow and credit the merchant twice. Both paths now re-read the reserve under a row lock and re-validate against the fresh state. PSP_FREEZE_RELEASE is not covered by the duplicate-fund-flow unique index (partial releases are legitimate), so this lock is the only guard against the double credit.

changelog.types.feature
changelog.categories.settlement

Add an independent balance-divergence oracle

New balance:check-divergence command independently recomputes each merchant's available balance from raw fund-flow amounts and alerts when the stored running balance has drifted. It catches the stored-balance drift class — including the case the existing balance-sum check misses, where the drift lands on the available entry but the chain tail self-corrects. Scheduled nightly at 00:45, before the recalc that would heal the drift.

changelog.types.fix
changelog.categories.settlement

Add unique-index backstops against duplicate fee and refund records

Companion to the fund-flow duplicate backstop: the same recovery-path race could also duplicate the per-attempt TransactionFee row or record the same PSP refund twice. Added a unique index on transaction_fees(payment_attempt_id) (one fee row per attempt) and a partial unique index on refunds(transaction_id, provider_id) where provider_id is not null (a given PSP refund recorded at most once per transaction; partial refunds with distinct provider_ids and pending refunds with a null provider_id remain allowed). Verified no existing rows violate either constraint before adding.

changelog.types.fix
changelog.categories.settlement

Add a unique-index backstop against duplicate success fund-flow entries

Attempt-success entries (PAYMENT, processing fee, MDR, settlement, reserve) and refund entries (REFUND, REFUND_FEE, REFUND_HOLD, REFUND_HOLD_RELEASE) are created exactly once per payment attempt / refund. A recovery-path race — the inquiry job and a PSP webhook resolving the same attempt concurrently — could insert a second set and double-credit the merchant. Added a PostgreSQL partial unique index on fund_flows(flowable_id, flowable_type, type) scoped to those one-per-flowable types, so the duplicate insert fails instead of double-crediting. Types that legitimately repeat per flowable (e.g. PARTNER_COMMISSION_REVERSAL partial clawbacks) are excluded. Verified no existing rows violate the constraint before adding it.

changelog.types.fix
changelog.categories.settlement

Add automatic deadlock retry to dispute resolution fund-flow writes

Added a WithTransactionRetry trait (runInTransaction() wrapping DB::transaction with a retry count) so money-path transactions survive a PostgreSQL deadlock (40P01) or serialization failure (40001) under concurrent webhook/inquiry/withdrawal load instead of surfacing a 500. Applied it to DisputeResolutionService::resolveDispute, whose closure is idempotent (re-reads state, idempotency-guarded fund-flow writes). The existing test-only DatabaseRetryServiceProvider never wrapped these transactions, so production had no deadlock retry on this path. Rollout to the remaining money paths (attempt-success fund flows, withdrawals) is paired with the per-site lock-ordering work.

changelog.types.fix
changelog.categories.fraud

Add per-provider cooldown to BIN lookup so a rate-limited API is skipped

BinLookupService tried BinList.net then BinLookup.io on every miss; when a provider returned 429 (rate limited) it was retried again on the very next lookup of a different BIN, hammering an API that had just throttled us. A provider that returns 429 or 503 now enters a 1-hour cooldown (cache key bin_api_cooldown:{provider}) and is skipped while cooling down, so traffic shifts to the other provider; if both are cooling down the lookup degrades to null, which routing already handles gracefully. Existing single-flight cache, negative cache, rate limits and failure-streak alerting are unchanged.

changelog.types.fix
changelog.categories.settlement

Fix lost dispute debiting the merchant principal twice

DISPUTE_HOLD freezes the full disputed amount when a dispute opens. On a WON dispute the hold is released (DISPUTE_WIN_RELEASE), but on a LOST dispute the hold was never released while a separate DISPUTE_LOSS was also created — so the merchant principal was debited twice (the orphaned hold plus the loss). For a paid 100 / refunded 30 transaction a lost dispute netted -190 instead of the correct -90. The hold is now treated as a provisional freeze: a lost dispute creates DISPUTE_HOLD_RELEASE to reverse the hold and records the actual loss via DISPUTE_LOSS once (the hold is released even when the chargeback is 0, e.g. already fully refunded, so it is never orphaned). The dispute fee remains non-refundable.

changelog.types.fix
changelog.categories.settlement

Require amount + fee when approving a withdrawal to prevent fee overdraft

Approving a withdrawal debits both the amount (PAYOUT) and the processing fee (OTHER_FEE), but the admin approval balance gate only required the amount. When the available balance covered the amount but not the fee — e.g. a refund or dispute landed between request and approval and dropped the balance — approval succeeded and pushed the merchant balance negative by the fee. The gate now requires available >= amount + total_fee (bcmath), rejecting approvals that would overdraw by the fee.

changelog.types.fix
changelog.categories.settlement

Fix refund hold release under-restoring when the refund is capped or partial

On the PSP refund webhook path, BaseGateway::processRefund caps/partials the refund amount and overwrites $refund->amount before releasing the REFUND_HOLD. releaseRefundHold() credited that mutated amount, so when the completed/capped refund (e.g. 250) was smaller than the originally-held amount (e.g. 300), the hold release restored only the smaller value — permanently over-debiting the merchant by the difference even though only the smaller amount was actually refunded (and fully stranding the held funds on a failed refund). releaseRefundHold() now restores the exact amount recorded on the REFUND_HOLD ledger row (negated), so the hold cycle always nets to zero regardless of later changes to $refund->amount.

changelog.types.fix
changelog.categories.payment

Prevent a late 3DS callback from overwriting an already-finalized transaction

A transaction can have multiple payment attempts (cascade/retry). When one attempt finalized the transaction (a sibling attempt succeeded, or a PSP webhook landed) while an earlier attempt was still in its 3DS round-trip, the late callback for that earlier attempt overwrote the transaction status with its own result — e.g. downgrading a settled SUCCESS to FAIL and emitting a spurious cascade/failure webhook. ThreeDSecureController::callback now locks and re-reads the transaction inside its DB transaction and skips the transaction-status update (and cascade) when the transaction is already terminal; the attempt still records its own outcome. Mirrors the existing guard in SweepStuckAuthenticatingTransactionsJob and PaymentController::confirmation.

changelog.types.fix
changelog.categories.settlement

Fix admin manual fund-flow adjustments failing with a foreign key error

fund_flows.adjusted_by referenced users(id), but the admin fund-flow endpoints (manual adjustment, void, correction) write the acting admin's id under the admin guard. Admin ids live in the admins table, so every admin manual adjustment/void/correction failed with a foreign key violation (HTTP 500) in production — the feature was effectively unusable. The foreign key now references admins(id) ON DELETE SET NULL, matching the sibling protest_fees.adjusted_by. The migration first nulls any adjusted_by value that is not a valid admin id so the new constraint applies cleanly to existing data.

changelog.types.fix
changelog.categories.settlement

Fix back-dated manual adjustment being dropped from merchant available balance

A back-dated manual fund-flow adjustment (released_at up to 90 days in the past) lands mid-chain, leaving every later entry's stored running balance stale. Because available balance reads the last released entry's stored balance rather than re-summing, the adjustment never reached the merchant's available balance. The admin create endpoint now rebuilds the full running-balance chain via recalculateBalanceForMerchant() whenever a released_at is supplied (a non-back-dated adjustment is the chain tail and is unaffected).

changelog.types.fix
changelog.categories.settlement

Fix merchant available balance inflating after editing a markup fee with later fund flows

FundFlowService::updateMarkupFeeFundFlow() reseeded the edited entry's running balance from the merchant's LATEST balance plus the amount delta, then rebuilt only the subsequent entries from that wrong seed. When any fund flow existed after the markup fee (e.g. a settlement release), this inflated the merchant's available balance by the sum of every later entry's amount — a phantom withdrawable credit. The edit now rebuilds the entire running-balance chain from genesis via recalculateBalanceForMerchant() (the same correct path voidFundFlow uses), so available balance reflects the true post-edit total. Added a regression test asserting available balance with a fund flow created after the markup fee.

changelog.types.fix
changelog.categories.security

Fix non-functional "remember me" on merchant and admin login

The "remember me" checkbox on both the merchant portal and the admin (operate) login had no effect: AppServiceProvider pinned the recaller cookie duration (auth.remember_duration) to 480 minutes, exactly equal to SESSION_LIFETIME, so the remember cookie expired together with the session and never kept the user signed in any longer. Raised the default remember duration to 1440 minutes (1 day) so it outlives the session, and documented that auth.remember_duration MUST exceed session.lifetime. Added RememberMeTest asserting the recaller cookie is issued with the correct longer lifetime for both the web and admin guards.

changelog.types.improvement
changelog.categories.dashboard

Filter admin transactions by alert severity and type

The admin transactions list now exposes 'Alerts' (severity) and 'Alert Type' (Fraud / Website Mismatch / Source Whitelist) filters in the desktop toolbar, and the CSV/Excel export honors them so the exported file matches exactly the filtered page. Previously these alert filters were reachable only via the mobile sheet or a URL parameter and were silently dropped from the export.

changelog.types.fix
changelog.categories.dashboard

Admin transaction export matches the filtered list (N=N)

The admin transactions export now contains exactly the rows the filtered page shows. Previously the export ignored the merchant, PSP, API-key, PSP-freeze and alert filters and the default last-7-days window, so a list filtered to N transactions could export far more. The index, the export record count and the export file now all apply one shared filter (AdminTransactionQueryFilter), so applying a filter that shows 50 rows on the page exports exactly 50 rows.

changelog.types.feature
changelog.categories.fraud

Source-site whitelist monitoring on transaction alerts

The admin transactions Alerts column now flags when a transaction's source site is not one of the merchant's registered/whitelisted domains. It checks the merchant-declared redirectUrl (critical) and websiteUrl (high) at create time across hosted, iframe and S2S flows, and the browser-observed referrer (low), iframe top/ancestor origin (high) and embedded Sec-Fetch context (medium) at checkout time. Each signal carries its own severity and appears as a 'Source Whitelist' category in the alert dialog. Monitor-only — transactions are never blocked. Also fixes foundational issues in the existing website-mismatch system: getRegisteredDomains now matches APPROVED websites + verified domains, domain matching is one-directional and normalized (closing a confusion/empty bypass), tracking rows are unique per transaction, and the registered-domain cache is invalidated on website changes.

changelog.types.feature
changelog.categories.dashboard

Per-merchant feature rollout gating

Added an entitlement layer that hides in-progress merchant-portal features until they are rolled out per merchant. A feature defined as 'testing' in config/features.php is hidden from the nav, the team permission picker (and role badges), and returns 404 on its routes/API for non-entitled merchants — enforced by the EnsureFeatureEntitled (feature.gate) middleware on MERCHANT entitlement (Laravel Pennant scope), backed by a route-coverage CI invariant test. Owners cannot grant a role carrying a gated permission. Operators roll features out with 'php artisan feature:rollout <feature> --merchant=ID' (or --all/--off/--list); effect is immediate and reversible. First gated feature: fraud-v2 (group 'fraud').

changelog.types.fix
changelog.categories.partner

Partner commission reversals are now idempotent per refund/dispute

A duplicate refund or dispute webhook, or a payout job retried after an interrupted run, could previously reverse the same partner commission more than once and over-claw the partner balance. Each commission is now reversed exactly once per triggering refund or dispute. Dispute loss and dispute-win fund-flow entries also guard against duplicate creation on a replayed resolution.

changelog.types.fix
changelog.categories.platform

Status page no longer reports healthy when its own check fails

The public status API returned 'operational' whenever its underlying health check threw an error (for example, the database being unreachable) — claiming all systems healthy precisely during an outage. It now degrades honestly instead. The 90-day uptime calculation also no longer mislabels a day as 'no data' based on a single trailing component's metrics.

changelog.types.fix
changelog.categories.api

Unified webhook event names across payment flows

Admin-recorded manual payments emitted 'payment.succeeded'/'payment.failed' while the normal PSP flow emitted 'payment.paid'/'payment_attempt.failed'. Because webhook subscriptions match the event name exactly, a merchant subscribed to one set silently missed the other. Manual payments now emit the same canonical event names as the standard flow.

changelog.types.fix
changelog.categories.api

Corrected webhook and SDK documentation

Developer docs taught webhook event names ('payment.succeeded/failed/processing') and a server-to-server endpoint path that did not match the live API. Docs now reflect the real event names ('payment.paid', 'payment_attempt.failed', 'payment.requires_action') and the correct '/v1/transaction/create-s2s' path; the unpublished PHP SDK install command is marked as coming soon.

changelog.types.fix
changelog.categories.marketing

Admins notified of new contact form submissions

Contact form submissions were stored but no one was alerted, so a lead could be missed despite the 'we will get back to you' confirmation shown to the visitor. Each submission now broadcasts an in-app notification to admins, linking to the submission.

changelog.types.fix
changelog.categories.partner

Partner commission not reversed on refund or dispute loss

When a transaction was fully or partially refunded, or lost as a dispute/chargeback, the partner's referral commission was left untouched, so partners kept commission on revenue that was clawed back. Commissions are now reversed pro-rata: pending commissions are voided and credited commissions are clawed back from the partner balance via a PARTNER_COMMISSION_REVERSAL entry (cumulative across partial refunds).

changelog.types.fix
changelog.categories.payment

Unify payment amount to dollars across S2S, hosted, HPP and iframe

Server-to-server payments previously accepted and stored the amount in cents while the hosted, HPP and iframe flows used dollars, so S2S transactions displayed 100x the real amount (e.g. $100 for a $1 payment) on dashboards, exports, merchant webhooks, redirect payloads and emails. The S2S API now accepts the amount in dollars like the other flows, stores it consistently, and converts to minor units only when calling the underlying PSP.

changelog.types.fix
changelog.categories.api

Sandbox gateway crashed on inbound webhook delivery

The sandbox gateway skipped its parent constructor, leaving the fee calculator uninitialized, so every inbound webhook to the sandbox PSP failed with a 500 error. The constructor now initializes the base gateway correctly.

changelog.types.fix
changelog.categories.settlement

PSP reconciliation never marked refunds and disputes as matched

Reconciliation wrote match/discrepancy status onto refunds and disputes through a guarded update that silently dropped the fields, so those records stayed unreconciled. Status is now persisted correctly.

changelog.types.fix
changelog.categories.payment

Email Try Again button linked to an unsigned checkout URL (403)

The customer payment-failed email built its retry link from the main app URL without a signature, so clicking Try Again hit the signed checkout route and failed with 403 Invalid signature. The link is now a temporary signed checkout URL on the gateway domain, and email templates only render the button when a signed URL is present.

changelog.types.fix
changelog.categories.payment

Fraud-block handler returned 500 instead of 403

Fixed an enum typo (CANCELLED vs CANCEL) in all three fraud-BLOCK branches of transaction creation. Blocked transactions now return a clean 403 FRAUD_BLOCKED response and are correctly marked as cancelled.

changelog.types.fix
changelog.categories.settlement

Daily settlement generation missed mid-day settlements

Settlement date-range queries compared full timestamps against date strings, so settlement records with a time-of-day component (all real PSP data) were silently skipped. Ranges now cover the full day for settlements, refunds, and disputes.

changelog.types.fix
changelog.categories.api

Sandbox inbound webhook endpoint crashed on delivery

The sandbox gateway parsed inbound webhooks with an outdated DTO signature, causing a 500 error on /webhook-receiver. Parsing now matches the current WebhookDTO contract.

changelog.types.feature
changelog.categories.platform

Merchants export dialog with column picker

Replace direct-download export on /operate/merchants with EnhancedExportDialog — supports column selection, CSV/Excel format, and async email mode. Skips unused correlated subqueries for faster exports.

changelog.types.feature
changelog.categories.payment

Simplify cascade configuration — smart priority order and remove stub toggle

The Cascade Configuration card no longer duplicates the Payment Methods drag order. When no custom cascade order is set, the service automatically follows the Payment Methods card order (api_key_method.priority). Admins can enable a custom override via toggle. The non-functional Notify on Cascade toggle has been removed.

changelog.types.feature
changelog.categories.security

Redirect proxy identity guard — CAT cookie and IP matching

Adds two opt-in identity checks to the redirect proxy to ensure only the buyer who opened checkout can resolve the redirect URL. The Checkout Access Token (CAT) is an encrypted HttpOnly cookie issued when checkout loads; the IP check validates the resolve request comes from the same IP stored in payment_attempts. Both checks use OR logic: passing either grants access; both failing returns 403. A per-transaction proxy_protected flag enables retroactive protection even after merchant proxy config is disabled. Includes admin toggles per transaction and a blocked-count metric per merchant config.

changelog.types.fix
changelog.categories.payment

Fix missing browserDetails in S2SPaymentRequestDTO constructor call

ArgumentCountError when submitting checkout-field form — $browserDetailsDto was built but not passed to S2SPaymentRequestDTO constructor (argument #26).

changelog.types.fix
changelog.categories.platform

Fix admin merchants page 340× faster balance queries

Resolved a PostgreSQL type mismatch between fund_flows.currency (varchar) and merchants.default_currency (char(3)/bpchar) that caused currency to be a post-scan Filter instead of an Index Cond on idx_fund_flows_completed. Fix: CAST(default_currency AS varchar) collapses per-page balance batch from 2,965ms to ~9ms and stats query from 48,263ms to 581ms (verified via EXPLAIN ANALYZE on staging with 5.7M rows). Also caches fee tiers, removes redundant withCount, pre-warms stats cache every 15 minutes.

changelog.types.fix
changelog.categories.platform

Upgrade laravel/horizon to v5.47.2 to fix queue worker crash-loop

laravel/framework v13.13.0 made the queue WorkCommand read the new --stop-when-empty-for option unconditionally, but laravel/horizon v5.45.4's horizon:work command signature did not define it, so every Horizon worker crashed on boot. Upgraded laravel/horizon to v5.47.2 (within the existing ^5.45 constraint), whose horizon:work signature includes --stop-when-empty-for, restoring queue processing.

changelog.types.fix
changelog.categories.platform

Suppress chargeback ratio alerts below minimum transaction volume

A chargeback ratio computed from a tiny denominator is statistically meaningless (e.g. 2 disputes / 3 transactions = 66%). ChargebackRatioMonitorService now suppresses alerts when a merchant has fewer than 50 successful transactions in the rolling 30-day window, matching how card-scheme programs (Visa VAMP / Mastercard ECM) only evaluate above a minimum volume. The real ratio is still reported; only the alert level is floored to 'normal'.

changelog.types.fix
changelog.categories.platform

Stop muted/acknowledged alerts from re-firing on a ~24h cycle

The hourly alert:auto-resolve sweep treated suppressed alerts as 'stale' because muting/acknowledging freezes last_alerted_at. It auto-resolved them, which cleared is_muted and let the next monitor run re-arm the alert — so a muted chargeback alert kept coming back roughly every 24h. AlertState::scopeStale now only matches genuinely-unsuppressed NEW alerts (excludes is_muted=true and ACKNOWLEDGED), so operator-silenced alerts stay silenced.

changelog.types.fix
changelog.categories.fraud

Fix privacy consent API — wrong named parameters and missing getAllowedCollection()

BehaviorLogController.recordPrivacyConsent() called PrivacyConsentService::recordConsent() with named parameter transaction: (passing a Transaction model) but the service expects transactionId: string. PHP 8 named argument mismatch caused 500 on POST /api/fraud/privacy-consent. Also: getConsentStatus() called getAllowedCollection() which did not exist on the service. Fixed by correcting the call to use the actual service signature and adding getAllowedCollection(string) to PrivacyConsentService.

changelog.types.fix
changelog.categories.platform

Surface container startup migration errors (Dokploy deploy debuggability)

Container entrypoint script was silently exiting in ~1.3s when Laravel migrate:status failed (e.g. phpdotenv parse error from unquoted .env values), because the `MIGRATE_STATUS=$(...)` assignment under `set -e` killed bash before the error-handler block could print the captured output. Wrapped the capture in `set +e`/`set -e` so failures now surface in container logs with the most common causes documented (phpdotenv parse error / DB unreachable / missing APP_KEY). Eliminates a major class of black-box Dokploy rollbacks.

changelog.types.fix
changelog.categories.platform

Document ALERT_TELEGRAM_WEBHOOK_SECRET in env templates

Both .env.staging.example and .env.production.example were missing the ALERT_TELEGRAM_WEBHOOK_SECRET placeholder needed for interactive alert buttons (Snooze/Mute/Acknowledge). Anyone provisioning a fresh VPS following the template silently ended up with non-functional buttons. Added the placeholder with inline instructions for generating the secret and registering the webhook.

changelog.types.fix
changelog.categories.platform

Fix interactive Telegram alert buttons (Snooze/Mute/Acknowledge) not persisting

Chargeback Telegram alerts kept re-firing every hour even after ops clicked the inline keyboard buttons because Telegram was never delivering callback_query updates. Two compounding bugs: (1) merchant bot webhook subscribed only to `message` updates when registered before the interactive alert feature shipped, so Telegram silently dropped button clicks; (2) telegram:set-alert-webhook printed SUCCESS in same-token mode without re-registering anything, hiding the misconfig. Fix: command now delegates to TelegramBotService::setWebhook in same-token mode so callback_query is subscribed when alerts.interactive.enabled=true; new telegram:webhook-status read-only health command surfaces the misconfig with non-zero exit; webhook controller now logs each callback_query received.

changelog.types.fix
changelog.categories.platform

Fix stale merchant balance shown in admin, reports and AI assistant

Merchant balance is now always derived from fund_flows (the single source of truth) instead of a deprecated, never-updated merchants.balance column. The admin merchant list/detail/analytics, KYB review, the daily Telegram report, the AI balance tool and the merchants export previously showed a stale or zero balance; they now reflect the real available balance per the merchant's default currency. The legacy column has been dropped.

changelog.types.fix
changelog.categories.dashboard

Fix retry-limits settings Save doing nothing

The Transaction Config → Retry Limits form posted to a non-existent route, so clicking Save Changes silently failed and merchants could not change their max retry attempts or cooldown period. The form now targets the correct route and saves persist.

changelog.types.fix
changelog.categories.payment

Unify card brand detection to single canonical service

Introduced CardBrandDetector as single source of truth for card brand detection, replacing 5 parallel implementations with divergent bugs. Fixes two production issues: (1) CardBrandEnum::UNKNOWN fatal Error at PaymentController:2446 causing runtime crash for JCB/Discover/UnionPay/Diners cards; (2) 'union-pay' vs 'unionpay' mismatch in BaseGateway causing UnionPay transactions to be stored as 'other'. Detection now covers all 7 brands with correct ISO 7812 BIN ranges including Mastercard 2-series (2221–2720, excluding Mir), JCB legacy BINs (2131xx, 1800xx), and CUP/Discover co-brand resolution via Discover rails.

changelog.types.fix
changelog.categories.platform

Fix BroadcastException when Reverb is not deployed on dedicated host

TransactionCreated and TransactionUpdated events were failing as queue jobs on staging because REVERB_HOST pointed to the web app domain. Added broadcastIf() guard: skips broadcasting when REVERB_HOST equals the app hostname, preventing BroadcastException and queue job failures until Reverb is deployed on its own server.

changelog.types.fix
changelog.categories.security

Fix SecurityMonitoring TypeError when User-Agent header is absent

isSimilarUserAgent() declared strict string parameters but $request->userAgent() returns ?string. Automated tools (curl, scanners) sending requests with no UA header caused TypeError. Fixed by adding null guards in detectUserAgentChange(): skip comparison and skip cache update when currentUserAgent is null. Added 3 regression tests covering null UA, cache preservation, and first-request UA caching.

changelog.types.fix
changelog.categories.security

Fix Merchant\WebsiteController::store() 500 + scope api_key_id to current merchant

Phase 4 plural endpoint POST /websites was missing api_key_id in inline $request->validate() rules, causing NOT NULL DB constraint to throw QueryException as unhandled 500 on every call (3× repeat alerts on 2026-05-19 from manual SSRF probe). Added required + Rule::exists('api_keys','id')->where('merchant_id', $merchant->id) validation — closes cross-merchant API key binding gap as defense-in-depth. Also wrapped WebsiteService::addDomain() call in try-catch (mirroring sibling removeDomain pattern) so the 5-changes-per-30-days rate-limit raw \Exception now renders as field error via back()->withErrors() instead of bubbling to exception reporter.

changelog.types.breaking
changelog.categories.payment

Tier-based fee configuration replaces per-(merchant, payment_method) JSON fees

BREAKING: Replaces per-(merchant × PM) JSON fee config with discrete tier templates (Standard/Premium/HighVolume/Enterprise) + per-(PM × currency) provider costs. Onboarding picks 1 tier instead of 5 JSON columns × N PMs. Schema drops merchant_method.{payment,refund,dispute,claim,protest}_fees, api_key_method.{payment,refund,dispute,protest}_fees, payment_methods.default_*_fees, global_fee_configs table. Adds merchant_fee_tiers, merchant_fee_tier_rates, payment_method_provider_costs (+ history tables for Decision #5 Option 3). FeeConfigResolver service replaces WithMerchantMethodFee trait. Decision #6 Option B fallback to tier code='standard' when merchant.tier_id NULL. New routes: /operate/fee-tiers CRUD, /operate/merchants/{id}/fee-tier (audit log). 14 commits, net -2329 lines.

changelog.types.fix
changelog.categories.dashboard

Fix 5 admin UI bugs — i18n, MobileCardList, currency, CaptchaConfig, error pages

CI-045: Contact form submit button was hardcoded English, now uses i18n key. CI-040: MobileCardList on 5 admin pages (Collusion, FraudSignals, DeviceFingerprints, Customers, FundFlows) was receiving :data= instead of :items= causing blank mobile cards. CI-034: Marketing Costs table displayed all amounts as USD instead of the row currency. CI-041: CaptchaConfig admin pages had 22 wrong i18n key paths (missing 18 translation keys); DataTable was missing selection-change emit. CI-038: 403/404 error pages redesigned — full dark mode, ApplicationLogo, DarkModeToggle, permission detail block, i18n, matching gradient theme.

changelog.types.improvement
changelog.categories.api

Rename X-Client-ID HMAC header to X-Api-Key-Id

BREAKING: The HMAC authentication header X-Client-ID has been renamed to X-Api-Key-Id across all environments (middleware, signature service, simulator, docs pages, E2E tests). Also completes the ApiClient→ApiKey rename refactor: renamed test directories (ApiClients/→ApiKeys/, ApiClientTable/→ApiKeyTable/), fixed cache keys (api_client_last_used→api_key_last_used, cascade_config:api_client→cascade_config:api_key), updated query param (api_client_filter→api_key_filter), and fixed a silent cache invalidation bug in CascadingPaymentService where the forget() path used the old key name.

changelog.types.fix
changelog.categories.dashboard

Fix KYB matrix mode — wire document upload, nav buttons, and review display end-to-end

4 bugs fixed in /kyb/create when a Business Category (e.g. Adult Content) is selected: (1) KybController::storeDocuments now processes dynamic industry document slots from DocumentMatrixService — previously all matrix-mode uploads were silently dropped; (2) Navigation buttons (Save Draft / Complete Application / Previous) now render in both matrix and legacy mode — they were inside the v-else block so invisible when matrix mode was active; (3) Step 5 Review now shows Country and Business Category names — fixed missing eager load + camelCase key mismatch (businessCategory → business_category); (4) Sidebar document progress is now matrix-aware via new useKybDocumentMatrixProgress composable that groups sections by personal identity + required/optional industry slots. Also: form correctly restores uploaded matrix files on Step 4 revisit (buildKybDocs extended); document_type_code column populated for admin/reporting queries; Zod schema + transformValuesForSubmit extended for dynamic slots; PERSONAL_IDENTITY_CODES const exposed as single source-of-truth from PHP via Inertia prop.

changelog.types.fix
changelog.categories.platform

Fix Docker production build — switch to debian:bookworm + packages.sury.org

Launchpad PPA (ppa.launchpadcontent.net:443) is blocked by RackNerd VPS provider, causing build:docker to timeout after 400s when installing PHP 8.4 packages. Fixed by switching Dockerfile.production base from ubuntu:24.04 to debian:bookworm-slim and PHP source from the blocked Launchpad PPA to packages.sury.org/php/bookworm (Ondrej Surý's own server, confirmed accessible). PostgreSQL repo updated to bookworm-pgdg. GPG key saved directly (binary format, no gpg --dearmor). Build time: 594s end-to-end.

changelog.types.fix
changelog.categories.platform

Eliminate 30s Redis exception window on Swarm rolling deploy

5 coordinated fixes eliminating 5-15 RedisException Telegram alerts per deploy: (1) Wait-for-Redis gate in entrypoint reads .env via grep (Dokploy shell env is empty) and blocks supervisord from starting horizon/reverb/scheduler until Redis overlay DNS converges; (2) Fix reverb.php REDIS_TIMEOUT cast bug (60→2.0s); (3) Add retry_interval=100ms to phpredis connections for sub-second blip absorption; (4) 3-attempt retry in CheckRedisHealth middleware; (5) Graduated readiness endpoint returns 200+degraded for Redis-only failures (prevents Swarm false-positive rollback) with database-backed counter alerting at 5 consecutive failures.

changelog.types.improvement
changelog.categories.dashboard

Unify KYB Business Category select with shadcn Select pattern (Years/Turnover)

Migrated /kyb/create Step 1 Business Category from custom Popover+Command to shadcn <Select>, matching Years in Business / Annual Turnover styling (h-12, border-2, rounded-xl, focus:border-blue-500, full dark mode). Added Required + Locked badges and proper vee-validate componentField binding. Side-fixes 2 hidden bugs: (1) kyb.field_locked i18n key was missing from en/vi settings.json — 5 KYB fields rendered literal 'settings.kyb.field_locked' text in refill mode because vue-i18n v9 t(key, fallback) does NOT accept a fallback string; (2) KybController eager-loads businessCategory relation, so props.kyb.business_category arrives as an OBJECT not a code string — the old select compared object to string and silently failed to restore the selected category after reload. New resetMerchantKybState() helper in kyb-helpers.js resets ACTIVE merchant fixture to REGISTERED + clears Kyb in beforeEach. E2E suite kyb-industry-category.spec.js: 5/5 PASS.

changelog.types.fix
changelog.categories.platform

Fix 500 error on KYB pages for newly registered merchants

Fixed enum vs string comparison in SettingController (statusColor always returned null), added null-safe operator for firstRegisteredEmail, fixed optional chaining in Index.vue. Added idempotent recovery migration to apply missed April 19 KYB structures if business_categories table is missing on staging. Added BusinessCategorySeeder to FoundationSeeder so reference data is always seeded.

changelog.types.deprecated
changelog.categories.platform

Remove redundant sandbox merchant test pages

Removed /sandbox/test-payment and /sandbox/testing merchant portal pages. These pages duplicated functionality already available through the checkout sandbox flow with the Sandbox PSP.

changelog.types.feature
changelog.categories.payment

Cascade deferred items: timeline, health dashboard, dispute attribution, QA seeder

U10: decline_code select in cascade error rule forms. D14: CascadeChainSeeder with 3 exemplar chains for QA. F5/X3: disputes.payment_attempt_id FK for cascade-aware chargeback attribution. U11: CascadeAttemptsTimeline Vue component in merchant transaction detail. O9: /operate/cascade/health admin dashboard with PSP pickup rates, exhaustion merchants, and hourly volume chart.

changelog.types.fix
changelog.categories.payment

Webhook concurrent race: lockForUpdate returns fresh Transaction

claimWebhookToken() now returns the freshly-locked Transaction from inside DB::transaction(), preventing stale in-memory reads on status, paid_amount, and refunded_amount after acquiring the row lock. All three callers (processPaymentAttempt, processRefund, processDispute) now reassign $transaction to the fresh instance, closing the concurrent webhook double-processing gap.

changelog.types.improvement
changelog.categories.payment

Cascade contract CI enforcement for all gateway implementations

AllGatewaysImplementCascadeContractTest auto-discovers every concrete gateway via glob() and uses PHP Reflection to verify mapDeclineCode() and getCascadeCapabilities() are overridden — not just inherited from BaseGateway. Fail prevents any gateway from deploying without explicit cascade mappings.

changelog.types.improvement
changelog.categories.payment

Cascade gateway capability gating and PSP rate protection

CascadeCapabilities DTO now derives gateway capability (S2S, hosted, async-webhook-only) from existing interface contracts — single source of truth, no array drift. RoutingValidator::canHandleCascadeContext blocks S2S-only PSPs from hosted cascade and async-webhook-only PSPs from S2S cascade. System-level kill switches (CASCADE_ENABLED, CASCADE_HOSTED_ENABLED, CASCADE_S2S_ENABLED) gate all cascade flows at shouldCascade() for instant ops rollback. PSP outbound rate bucket (Laravel RateLimiter, 1s decay, configurable per-PSP RPS cap) prevents cascade storms from self-DDoS-ing backup PSPs.

changelog.types.feature
changelog.categories.api

S2S idempotency key middleware (X-Idempotency-Key)

Merchants can now safely retry POST /api/v1/transaction/create-s2s by sending an X-Idempotency-Key header (32-64 chars). The middleware caches the first response in Redis (30-min TTL, namespaced per merchant) and replays it byte-identical on retry. A request fingerprint (amount + currency + ref + card_last4) guards against key reuse with mutated payload (returns 422 IDEMPOTENCY_KEY_CONFLICT). PENDING_3DS responses are not cached. Cache failures fail closed with HTTP 503. The key is persisted on the hop-0 payment_attempt and forwarded along the cascade chain by the orchestrator. Gated behind payment.cascade.idempotency_enabled (default ON).

changelog.types.feature
changelog.categories.platform

Geographic restrictions for payment methods (admin-only)

Admins can now define allowed/blocked countries per payment method with allow/block mode toggle. Routing engine enforces country at method level using billing country. BIN country enrichment (8-digit) and IP geolocation add additional fraud signals. Routing rule builder hidden from merchant portal — managed centrally by admin team. Cascade and fallback paths both enforce country restrictions.

changelog.types.feature
changelog.categories.fraud

BIN country enrichment for routing context

RoutingContext now includes bin_country sourced from BIN lookup (8-digit BIN support for Visa/MC 2022 rollout). Disagreement between billing, IP, and BIN country sources emits COUNTRY_SOURCE_MISMATCH fraud signal asynchronously via queue. Prepaid cards and sandbox PSP skip the signal to avoid false positives. Single-flight Cache::lock prevents external API stampede on cache miss.

changelog.types.fix
changelog.categories.platform

Cache invalidation back-fill for payment method config updates

Back-fill Cache::forget('payment_method:{id}') into updateCardBrands() (pre-existing bug — 1h stale config after admin update). Optimistic locking (last_updated_at check) prevents concurrent admin overwrite with 409 Conflict response. CountryCodeNormalizer added for Kosovo/disputed territory ISO code normalization.

changelog.types.feature
changelog.categories.payment

Sweeper for stuck 3DS authenticating transactions

A scheduled job runs every 5 minutes to mark transactions stuck in AUTHENTICATING status (no 3DS callback received within 15 minutes) as failed and fire the merchant failure webhook. The 3DS callback handler now silently no-ops if the attempt has already been swept, preventing split-brain races. Gated behind payment.cascade.sweeper_enabled.

changelog.types.feature
changelog.categories.payment

S2S synchronous cascade orchestrator (Phase 3)

Server-to-server payments now cascade synchronously across configured PSP fallbacks within a single HTTP request. The orchestrator enforces a 25-second wall-time budget, marks the prior attempt FAILED before each hop so late webhooks are rejected as stale, performs lineage and event writes in a single short DB transaction, and decorates the response with opaque per-hop psp_codes and a cascade_group_id only when more than one hop ran. Single-attempt requests keep the legacy response shape (additive-only). Gated behind payment.cascade.enabled and payment.cascade.s2s_enabled.

changelog.types.improvement
changelog.categories.security

Redirect Proxy ops alert on Redis fallback

When Redis is unavailable and the proxy token generator fails, EPaySe now raises a Telegram alert in addition to the error log so operators learn about proxy degradation immediately. The alert is dispatched after the surrounding DB::commit() via DB::afterCommit() so the HTTP call never holds an open database transaction. Payment flow continues uninterrupted via the existing fail-open fallback.

changelog.types.improvement
changelog.categories.platform

Redirect Proxy — psp_domain exposed to gateways

Added BaseGateway::getMerchantDomain() helper so PSP adapters that support a merchant_domain field can read the merchant's configured PSP Domain. Admin UI labels updated to reflect that the field is opt-in per PSP.

changelog.types.fix
changelog.categories.dashboard

DataTable component — opt-in default sort column

The shared DataTable.vue component previously hardcoded { id: 'created_at', desc: true } as its initial TanStack sorting state, causing '[Table] Column with id created_at does not exist' console warnings on tables that use a different timestamp column (e.g. updated_at). DataTable now accepts a defaultSort prop; the Redirect Proxy admin list opts in with updated_at. Backward compatible — existing consumers keep the created_at default.

changelog.types.improvement
changelog.categories.dashboard

Industry-driven KYB form fully activated

Removed feature flag and enabled industry-specific document requirements for all merchants. Fixed security gap where industry form branch lacked file validation.

changelog.types.feature
changelog.categories.dashboard

Merchant self-service cascade error rules

Merchants can now create, edit, and manage error pattern rules that control PSP cascade behavior from the Transaction Config dashboard. Merchant rules take highest priority in the evaluation chain: merchant-specific → admin PSP-specific → admin global. Regex match type is restricted to admin-only for security.

changelog.types.feature
changelog.categories.dashboard

Redesigned KYB wizard with resume-by-email

KYB onboarding is now a 5-step wizard with real-time progress save, email magic-link resume, mobile camera capture for documents, and a review page with e-signature attestation before final submission.

changelog.types.improvement
changelog.categories.dashboard

Industry-specific KYB document requirements

KYB form now adapts to your business category — only relevant documents are requested, reducing onboarding time for low-risk industries. High-risk industries receive clear guidance on required compliance documents.

changelog.types.fix
changelog.categories.payment

Financial precision: withdrawal fee calculation — bcmath chain (session 14)

WithdrawalController::store() computed withdrawals.total_fee using native PHP arithmetic operators (+, *, /) on amounts from DB (string numeric) and config (integer), causing silent float coercion before DB write to numeric(36,18) column. Fix: cast $amount to string at extraction point, replace all fee arithmetic with bcmul/bcdiv/bcadd chain, replace round($totalFee, 2) with bcadd($total, '0', 2). Also added explicit (string) cast for fee_amount DB write. Final comprehensive scan of all app/ directories (Traits, Observers, Listeners, Jobs, Commands, Models) confirmed 0 additional CRITICAL violations — CAST-AS-TEXT audit exhaustively complete across 14 sessions.

changelog.types.fix
changelog.categories.payment

Financial precision: dispute fees, BuyerShield, manual payments, refund guards (session 8)

Extended CAST-AS-TEXT audit from DB-level sums to float parameter types and native arithmetic operators (+, -, *, /). 19 CRITICAL violations across 9 files: (1) DisputeCreationService::calculateReversalFees() — 7 native float ops → bcmath; chargeback_amount round() → bcsub; correct bcmath implementation already existed in PaymentFeeCalculator. (2) BuyerClaimResolutionService::calculateReversalFees() — same pattern, 7 float ops + (float) cast on resolved_amount. (3) FundFlowService::createCorrectionEntry() — float $correctedAmount → string; FundFlowController (float) cast → (string). (4) MarkupFeeService chargeMarkupFee/updateMarkupFee — float $amount → string; abs() < 0.01 → bccomp abs. (5) ReconciliationFundFlowService — abs((float) $pendingFlow->amount) → bcmath; float $amount → string. (6) ManualPaymentAttemptService — 4 methods, floatval() → (string), feeds paid_amount DB column. (7) BuyerClaimService — ?float $amount → ?string for claim_amount DB column. (8) BuyerClaimController (string) cast added. (9) BaseGateway::processRefund() — float $refundAmount → string.

changelog.types.fix
changelog.categories.platform

Financial precision: AI tool settlement sums (session 7)

GetSettlementData AI tool — two DB-level sums on settlements.net_amount and gross_amount fed into bcadd/bcdiv currency conversion chain. Replaced with CAST(COALESCE(SUM(amount), 0) AS TEXT) pattern. Exhaustive scan of all remaining ->sum() calls in app/ completed — remaining MEDIUM violations are display-only (number_format, analytics charts) and do not feed bcmath chains.

changelog.types.fix
changelog.categories.platform

Fix CI-035: reserves export 504 timeout — correlated subquery → LEFT JOIN

ReservesExport::buildOptimizedQuery() had a correlated subquery in the SELECT clause: (SELECT t.ref FROM transactions t WHERE t.id = mr.transaction_id LIMIT 1). With 218,592+ reserve rows, this executed once per row (N×1 queries), causing Gateway Timeout. Fixed by replacing with LEFT JOIN transactions t ON t.id = mr.transaction_id + t.ref as transaction_ref. The buildSearchCondition() EXISTS subquery (for search-only, not per-row) remains unchanged. 31/31 ReserveControllerTest pass.

changelog.types.fix
changelog.categories.platform

Fix CI-036: buyer claims export DISTINCT/ORDER BY PostgreSQL error

BuyerClaimsExport::buildOptimizedQuery() used SELECT DISTINCT without bc.created_at in the column list, but BaseExport::applySorting() adds ORDER BY bc.created_at DESC. PostgreSQL strict: all ORDER BY expressions must appear in SELECT list when using DISTINCT (unlike MySQL). Fixed by adding bc.created_at to the SELECT DISTINCT column list.

changelog.types.fix
changelog.categories.platform

Fix EG-021: self-host Inter/Figtree fonts via @fontsource, remove external CDN

Removed external font CDN references (fonts.bunny.net) from app.blade.php and admin.blade.php. Fonts are now bundled via @fontsource/inter and @fontsource/figtree npm packages imported in app.js and admin.js. Fixes: (1) CSP font-src violations when CDN is blocked, (2) Playwright browser_navigate timeout from 7-12s per-file CDN latency, (3) external dependency for font loading.

changelog.types.fix
changelog.categories.security

Fix EG-021b: add localhost:5173 to font-src CSP in Nginx conf for 3 subdomains

docs.epayse.local, gateway.epayse.local, and status.epayse.local use hardcoded Nginx-level CSP that bypassed Laravel middleware updates. Added http://localhost:5173 to font-src directive in all 3 Nginx conf files to allow Vite dev server fonts.

changelog.types.fix
changelog.categories.dashboard

Fix CI-033: StatsCarousel wrong prop name (:items → :stats) in Invoice/Index.vue

Invoice/Index.vue was passing :items='statsCards' to StatsCarousel component, but the component expects :stats prop. Fixed prop name.

changelog.types.fix
changelog.categories.dashboard

Fix CI-034: InvoiceTable default sort column created_at → invoice_number

InvoiceTable/DataTable.vue had initial sort { id: 'created_at', desc: true } but created_at is not a valid accessorKey in Invoice column definitions. Also Invoice/Index.vue had defaultField: 'created_at'. Both changed to invoice_number (valid accessorKey and allowed sort column in InvoiceController). Prevents TanStack Table sort error.

changelog.types.fix
changelog.categories.platform

Financial precision: 7 additional sum() violations fixed in API, Telegram, and AI tools

Session 5 CAST-AS-TEXT audit. Schema correction: transactions.amount and refunds.amount are numeric(36,18), not integer — all monetary columns in this codebase use numeric(36,18). Fixed 7 CRITICAL violations where DB-level float sums fed bcmath: (1) MerchantSettingsController — fund_flows SUM for API balance response feeds bcadd currency conversion chain. (2) SettlementCurrencyService — fund_flows SUM for getMerchantBalanceByCurrency feeds getMerchantTotalBalance bcadd. (3) SendTelegramDailyReport — refunds.amount sum feeds bccomp/bcadd in report; transaction CASE-SUM for revenue feeds bcadd. (4) ComparePeriodsData AI tool — two transactions.amount sums feed full bccomp/bcdiv/bcmul chain for period-over-period comparison. (5) GetRevenueData AI tool — transactions SUM feeds bcadd/bcdiv per-currency metrics. (6) GetPartnerData AI tool — FIXED wrong column (amount → commission_amount) AND added CAST AS TEXT; wrong column caused silent PostgreSQL errors. All 26 SettlementCurrencyServiceTest pass.

changelog.types.fix
changelog.categories.platform

Financial precision: 11 additional sum() violations fixed across reports, verifier, and refund guards

Extended CAST-AS-TEXT audit to all 58 numeric(36,18) columns across 15 tables. Fixed 11 HIGH violations where DB-level float sums fed bcmath chains: (1) PartnerCommissionService — commission_amount sum guards monthly cap via bcsub (cap bypass risk). (2) PlatformPnLService — total_internal_fee, total_provider_fee, and fund_flows.amount sums (with explicit (float) cast) feed entire P&L calculation chain. (3) PspPerformanceService — paid_amount and total_internal_fee sums feed bcadd-based metrics. (4) FinancialIntegrityVerifier:1295,1323,1546 — verifier used float sums as reference values for bcsub-based discrepancy detection (defeating its own precision checks). (5) RefundService, ProcessClaimRefund, StoreRefundRequest — pending refunds sum guards over-refund prevention via bcsub. Also fixed 4 TreasuryController fund_flows display sums. All tests pass.

changelog.types.fix
changelog.categories.platform

Financial precision: 5 DB-level sum() violations fixed across financial services

Continued CAST-AS-TEXT audit on numeric(36,18) financial columns. Fixed 5 DB-level ->sum('amount') calls that returned PHP float, losing precision on 35B+ totals: (1) FinancialIntegrityVerifier.php:757 — platform_float balance check feeds bcadd. (2) ReconciliationService.php:154 — balance validation with bccomp(..., 18) — 18 decimal places of comparison requires full precision. (3) GetPlatformFinancialsData.php:67 — fund_flows sum for AI P&L reporting. (4) MarkupFeeService.php:182 — markup_fees.amount is also numeric(36,18) (same DB schema as fund_flows). Return type changed float→string. (5) FundFlowReportService.php:120 — opening balance sum feeds bcadd(opening, netChange, 18). All 123 related tests pass (359 assertions).

changelog.types.fix
changelog.categories.platform

Financial integrity verifier — 3 additional false-positive checks fixed, 30 tests

Continued audit of FinancialIntegrityVerifier staging false positives: (1) amount_consistency (additional fix): REFUND query lacked psp_reference filter — seeder-created refunds had r.amount stored in dollars instead of cents. Added whereExists subquery through r.transaction_id → payment_attempts → psp_reference. (2) fund_flow_vs_transaction_fee (HIGH, 200): seeder creates TransactionFees without fund flows — added whereNotNull('pa.psp_reference'). (3) transaction_fee_completeness (HIGH, 200): seeder creates SUCCESS payment_attempts without TransactionFee records — added whereNotNull('pa.psp_reference'). Total false positives eliminated: 2400+ HIGH/CRITICAL → 0. Added 2 new tests (total: 30 tests, 81 assertions). Remaining 25 issues in staging are known seeder data quality artifacts.

changelog.types.fix
changelog.categories.platform

Financial integrity verifier — 4 staging false-positive checks fixed

FinancialIntegrityVerifier was generating ~1500+ false-positive alerts on staging: (1) amount_consistency (CRITICAL, 200): verifier compared fund_flows.amount [dollars] with refunds.amount [cents] directly — 100x unit mismatch. Fixed by dividing r.amount by 100.0 in SQL and bcdiv(..., '100', 18) in PHP. (2) fund_flow_completeness (HIGH, 973): 297,848 seeder-created payment_attempts have no fund flows (StagingSeeder bypasses service layer). Added whereNotNull('pa.psp_reference') filter — real PSP-processed attempts always have psp_reference. Zero real transactions are missing fund flows. (3) settlement_timing_accuracy (HIGH, 500): seeder sets transaction_fees.settlement_at to fixed date, fund_flows.released_at to arbitrary value — gaps up to 71 days. Same psp_reference filter applied. (4) reserve_release_timing (HIGH, 500): same seeder artifact root cause, same fix.

changelog.types.fix
changelog.categories.platform

Financial integrity verifier false positives eliminated — REFUND, PROCESSING_FEE, MDR

FinancialIntegrityVerifier was triggering hundreds of false HIGH alerts per day. Root causes: (1) morphMap added 'refund'/'dispute' aliases without data migration — verifier queries using FQCN missed 30K rows with aliases. Fixed with whereIn(both formats) + chunked migration (FOR UPDATE SKIP LOCKED). (2) Zero-fee merchants have no PROCESSING_FEE/MDR fund flows (DB constraint prevents zero-amount entries). Fixed by joining transaction_fees and conditionally requiring fee flows only when total_fees > 0. (3) TransactionFactory::withPartialRefund/withFullRefund bypassed service layer — fixed to call FundFlowService::createRefundCompleteEntries after factory creation.

changelog.types.fix
changelog.categories.platform

Platform float precision loss fixed — CAST(SUM AS TEXT) replaces ->sum('amount')

FundFlowService::getPendingSettlement, getReserveBalance, getSystemBalance, and getPspFreezeReserveBalance all used Eloquent ->sum('amount') which PHP cast to float, losing precision on amounts > 10 billion (IEEE 754 double only has 15-17 significant digits). FinancialIntegrityVerifier::verifyPlatformFloat also affected. Fixed all 6 locations with selectRaw('CAST(COALESCE(SUM(amount), 0) AS TEXT) as precise_sum')->first()?->precise_sum. Also replaced abs() with bcmul($n, '-1', 18) for consistency.

changelog.types.fix
changelog.categories.payment

S2S paid_amount zero — fund flows and refund validation now correct

S2S (server-to-server) payments were not setting paid_amount on payment_attempts or transactions after PSP response. ProcessSandboxWebhook read paid_amount=0, creating zero-amount fund flows. RefundService compared dollars against cents (10000 vs 80.00), allowing over-refunds silently. Fixed by storing paid_amount in dollars (cents÷100) at S2S response time.

changelog.types.fix
changelog.categories.payment

Double KYB notification — removed duplicate call from controller

KYB approve/reject controller explicitly called notifyKybApproved/Rejected AND the KybObserver also fired on model update, sending two notifications. Removed the explicit controller calls; observer is now the single source of notification side effects.

changelog.types.fix
changelog.categories.payment

Float arithmetic in dispute chargeback calculation replaced with bcmath

DisputeResolutionService used round($paid - $refunded, 2) for chargeback amount — vulnerable to IEEE 754 precision loss on large amounts. Replaced with bcsub() and bccomp() for PCI-safe precision.

changelog.types.breaking
changelog.categories.api

browserDetails fields renamed to camelCase and all required

S2S API browserDetails nested field names changed from snake_case to camelCase (e.g., user_agent → userAgent, screen_width → screenWidth). All 12 fields are now required. New Browser Details JS SDK available at /js/epayse-browser.js for easy collection.

changelog.types.feature
changelog.categories.payment

Refund Approval Workflow & Admin Queue

Added admin refund approval queue with approve/reject/retry actions, auto-refund toggle per merchant (for PSPs without refund API), REFUND_HOLD fund flow for balance reservation, Telegram notifications for refund/chargeback events, balance-insufficient handling with admin intervention, and per-merchant refund configuration in admin settings.

changelog.types.feature
changelog.categories.payment

Merchant self-serve PSP onboarding requests

Merchants can now submit access requests for additional payment service providers from their dashboard. Admins review, approve (auto-assigning the PSP), or reject requests with notes. Includes full status workflow: pending → under_review → approved/rejected.

changelog.types.fix
changelog.categories.settlement

Fix 7 financial accuracy bugs in settlement, fees, and fund flow

Centralized fee calculation into PaymentFeeCalculator (was duplicated in 4 places with float bugs), fixed GlobalFeeConfig to use bcmath, added disputes.amount column enabling DISPUTE_HOLD/WIN_RELEASE, fixed balance inflation (total doubled after T+N), fixed DailyCalculation to use disputes.amount, added lazy PENDING→COMPLETED transition for settlement release, optimized getSystemBalance from O(N) to O(1) with DISTINCT ON + Redis cache.

changelog.types.feature
changelog.categories.platform

Add financial operations monitoring and management tools

Settlement-ledger auto-reconciliation (nightly), chargeback ratio monitoring with Visa VAMP/Mastercard ECM threshold alerts, manual payout creation for admin, treasury dashboard with platform-wide balance overview, admin audit log viewer, and cross-merchant P&L report with monthly breakdown.

changelog.types.feature
changelog.categories.platform

Ops Hub - Centralized DevOps Dashboard

Added Ops Hub page in admin portal (System > Ops Hub) with categorized links to all monitoring and developer tools (Horizon, Log Viewer, Dozzle, Uptime Kuma, Beszel, GlitchTip, Dokploy, Telescope, Mailpit). Features environment-aware URL resolution, live system health panel, and access type badges.

changelog.types.feature
changelog.categories.platform

Provider-agnostic AI Chat with pre-fetch data pattern

Refactored AI Chat features (KYB AI Chat, Platform AI Chat) to use provider-agnostic pre-fetch pattern. Claude CLI is primary (flat-rate subscription), with Gemini/OpenAI/Anthropic API as fallback. ContextProvider interface pre-fetches data from DB, injects into prompt, eliminating tool-calling dependency. Includes AbortController for SSE cleanup, typewriter effect for UX, unique delimiters for prompt security, partial tool failure resilience, and claude-cli-prefetch-pattern skill.

changelog.types.fix
changelog.categories.platform

Fix AI Ops hardcoded /operate URL prefix and feedback route issues

Replaced hardcoded /operate/ URLs in 4 AiOps Vue components with Ziggy route() helper for subdomain routing compatibility. Fixed KYB Intelligence feedback controller to return RedirectResponse instead of JsonResponse for Inertia compatibility. Fixed ScopeValidator rejecting plural keywords (merchants, transactions) by adding admin-specific keyword list.

changelog.types.fix
changelog.categories.security

Fix AI Chat Widget server hang on close/re-open

Added AbortController to useStreamingChat composable. Closing chat now aborts in-flight SSE fetch requests, preventing PHP worker exhaustion (504). Added 90s timeout on fallback axios calls.

changelog.types.improvement
changelog.categories.platform

Restructure seeders into 3-tier architecture with fund flow production parity

DatabaseSeeder now lean (~37s, foundation + accounts only). StagingSeeder provides full demo data with 7 new feature seeders. MassTransactionSeeder generates 7 fund flow types per transaction matching production FundFlowService, creates merchant_reserves records, uses bcmath scale 18. 15 unit tests with 252 assertions verify balance integrity.

changelog.types.feature
changelog.categories.platform

AI Operations Platform — Fraud Intelligence + Unified Dashboard

Phase 3: Fraud Intelligence with FraudIntelligenceReport model, FraudIntelligenceService (risk analysis, rule suggestions), GetFraudIntelligenceData AI tool integrated into AdminAnalyticsAgent. Phase 4: Unified AI Ops Dashboard with cross-domain stats (KYB, Fraud, Website, Learnings), activity feed, Learnings management page, and Fraud Intelligence index/detail pages. Admin sidebar AI Operations section with 4 sub-items. 15 PHPUnit + 53 Vitest + 8 E2E tests.

changelog.types.feature
changelog.categories.platform

Add Telegram AI merchant support bot

AI-powered Telegram bot for per-merchant group support. Hybrid LLM (Gemini for data queries + Claude CLI for integration docs), 15 AI tools, 2-phase comfort response, /sandbox command with email credentials delivery, webhook security with dual verification, per-group rate limiting, and docs extraction pipeline.

changelog.types.feature
changelog.categories.platform

CI/CD pipeline Telegram notifications

Real-time Telegram alerts for every CI/CD pipeline stage (deploy start/success/fail, smoke test results) with multi-channel architecture supporting separate channels for CI/CD, app alerts, and monitoring

changelog.types.feature
changelog.categories.platform

Comprehensive system error Telegram alerting

Fix broken telegram logging channel, add global exception and queue failure handlers, PSP gateway error alerts, fraud BLOCK/REVIEW notifications, DDoS detection alerts, security monitoring Telegram channel, fund flow discrepancy alerts, and scheduled task failure monitoring — coverage from 7 scenarios to near 100% critical paths

changelog.types.fix
changelog.categories.platform

Fix route cache duplicate names breaking staging deploy

Renamed sandbox.* route names to gateway.sandbox.*, main.sandbox.*, merchant.sandbox.* to prevent artisan route:cache failure on staging/production

changelog.types.fix
changelog.categories.dashboard

Fix merchant statements page crash on empty currency config

Fixed dead code toArray() ?? fallback and added null safety guard for merchants without currency configurations

changelog.types.fix
changelog.categories.platform

Fix CollectStatusMetrics missing Request parameter

Status metrics collection command failed after health monitoring auth was added, now creates internal authenticated request

changelog.types.improvement
changelog.categories.platform

Add 43 production readiness regression tests

19 PHPUnit tests verifying security config, bcmath enforcement, HTTP timeouts, onOneServer scheduling + 24 Playwright E2E staging tests for Sections 37-41

changelog.types.feature
changelog.categories.marketing

AI Agent Phase 4 — Social Autopilot, sub-agent orchestration & weekly learning aggregation

SocialAutopilotAgent with 4 capabilities (analyze, generate social content, approve content, generate report). ApproveContentHandler delegates to SocialContentService::approve(). SubAgentCallHandler enables parent-child agent session spawning via AgentOrchestratorService. GenerateAgentLearningsJob runs weekly to aggregate cross-session learnings (send times, content patterns, channel effectiveness). All 6 agents + 17 handlers fully registered. 8 new tests.

changelog.types.feature
changelog.categories.marketing

AI Agent Phase 3 — Churn Prevention, Performance Optimizer & auto-trigger

ChurnPreventionAgent (7 capabilities) and PerformanceOptimizerAgent (5 capabilities). 4 new handlers: DetectChurnHandler, AdjustBudgetHandler, PauseCampaignHandler, SendCampaignHandler — all delegating to existing services. AutoTriggerChurnPreventionJob runs daily at 09:00 to detect at-risk merchants and auto-start retention campaigns. 20 new tests.

changelog.types.feature
changelog.categories.marketing

AI Agent Phase 2 — Content Creator & Lead Nurturing agents

ContentCreatorAgent (6 capabilities) and LeadNurturingAgent (6 capabilities). 4 new action handlers: GenerateSocialContentHandler, CreateDripCampaignHandler, EnrollMerchantsHandler, ScoreLeadsHandler — all delegating to existing SocialContentService, DripCampaignService, and LeadScoringService. 18 new tests.

changelog.types.fix
changelog.categories.marketing

AI Agent Phase 1.5 — critical infrastructure bug fixes

Fix WebSocket event name mismatch (broadcastAs vs composable listen). Wire up MarketingAgentServiceProvider to register agents at boot. Dispatch AgentAutoApprovalTimeoutJob for review-level actions. Close learning feedback loop via recordSessionLearnings() on session completion. Remove dead Create.vue page. 14 new tests.

changelog.types.feature
changelog.categories.marketing

AI Agent Marketing Automation — Core Framework & Campaign Orchestrator

Autonomous campaign orchestration with 3-tier approval matrix (Auto/Review/Critical), real-time WebSocket timeline via Laravel Reverb, 7 action handlers (analyze, generate, create template/campaign/experiment, schedule, report), human-in-the-loop approval flow, and CampaignOrchestratorAgent as first concrete agent. 3 database tables, 6 enums, 6 core services, 3 Vue pages with live action timeline, useAgentSession composable. 89 BE + 65 FE tests.

changelog.types.feature
changelog.categories.marketing

Marketing Intelligence Phase 8 — batch operations, notifications, snooze & performance

Batch accept/dismiss for multiple recommendations with floating toolbar and confirmation dialog. Snooze action extends expires_at by 7 days. NewRecommendationsNotification dispatched after daily generation. Weekly marketing digest email job scheduled Monday 9AM. Stats caching (5 min TTL) with cache-busting on mutations. Composite database indexes on marketing_recommendations (status+created_at, status+expires_at). Select-all checkbox with ring highlight on selected cards. 20 BE + 7 FE new tests.

changelog.types.feature
changelog.categories.marketing

Marketing Intelligence Phase 7 — Claude CLI integration & AI content generator

Add ClaudeCodeService wrapping Claude Code CLI for flat-rate AI generation (Claude Max). Refactor AiInsightService with dual provider support (claude_cli vs anthropic_api). New AiContentGeneratorService with 4 methods: email subjects, email body, landing copy, ad copy — all bilingual EN/VI with JSON parsing and code block extraction. Content Generator page with dynamic forms, copy-to-clipboard, and loading states. 70 BE + 15 FE Vitest tests.

changelog.types.feature
changelog.categories.marketing

Marketing Intelligence Phase 6 — action automation, audit trail & detail page

Implement 3 missing apply methods (CreateDripCampaign, AdjustScoring, SendReEngagement) in CampaignOptimizerService. Add RecommendationAuditLog model with ULID prefix ral_ for tracking accept/dismiss/apply/rate actions. New Intelligence Show page with badges, star rating, analysis/outcome data, activity timeline, related recommendations and dismiss dialog. 35 Vitest + 16 PHPUnit tests.

changelog.types.improvement
changelog.categories.marketing

Sync Intelligence page UI/UX with Admin Portal conventions

Responsive header with mobile breakpoints matching 110+ admin pages. Replace manual pagination loop with standard Pagination component (supports ellipsis, Go to page, filter preservation). Mobile-optimized recommendation cards and touch targets. Fix Playwright MCP Chrome session conflict with --isolated flag and session-start cleanup hook.

changelog.types.feature
changelog.categories.platform

Buyer Shield — claim fees & claim_reference removal

Remove human-readable claim_reference (RSLV-XXXXXX) in favor of standard ULIDs. Add configurable claim fees per merchant per payment method (mirroring dispute fees). Fee charged on claim resolution (5 paths). CLAIM_FEE fund flow entries. Settlement NET formula updated to 7 components. Admin UI fee config tab. 34 new tests (18 BE + 16 FE).

changelog.types.feature
changelog.categories.platform

Buyer Shield — plan gap completion (evidence upload, bulk ops UI, email i18n)

Evidence upload routes for buyer and merchant with file validation and IDOR protection. Admin bulk operations UI with checkbox row selection, floating action bar, assign/arbitrate dialogs. Bilingual Vietnamese sections in buyer-facing email templates. E2E buyer claim lifecycle tests (12 tests). Evidence upload feature tests (10 tests).

changelog.types.feature
changelog.categories.platform

Buyer Shield Phase 4E — bulk operations & claim rate analytics

Admin bulk assign (escalated claims to reviewer) and bulk arbitrate (approve/deny multiple claims). Merchant claim rate analytics with risk level badge (Low/Medium/High) and 30-day trend detection on admin merchant detail page. 11 new tests.

changelog.types.feature
changelog.categories.platform

Buyer Shield Phase 4D — i18n & merchant response templates

Full i18n for 6 Resolve Vue pages and ResolveLayout (~180 translation keys, EN + VI). Config-based merchant response templates (6 presets: full refund, partial, reject) with quick-apply UI in claim response form. 3 new response template tests.

changelog.types.feature
changelog.categories.platform

Buyer Shield Phase 4 — auto-resolution & webhook events

Auto-resolution engine for claims below merchant's auto_refund_threshold (synchronous check at filing time, async PSP refund via ProcessClaimRefund). Webhook event system dispatching claim.filed, claim.auto_resolved, claim.escalated, and claim.resolved events via ProcessSendClaimWebhookToMerchant job with exponential backoff. WebhookPayloadBuilder extended with buildClaimObject() for Stripe-like event payloads. 19 new tests (8 auto-resolution + 11 webhook) across 2 test files.

changelog.types.feature
changelog.categories.security

EPaySe Buyer Protection (Buyer Shield) Phase 1 MVP

Buyer-facing dispute resolution portal with OTP/magic link verification, claim filing, and status tracking. Includes admin claim management with export, merchant dashboard claims view, SHA-256 token hashing, HKDF-derived session keys, TOCTOU protection, rate limiting, and IDOR prevention. 88 tests across 6 test files (161 total with related tests).

changelog.types.feature
changelog.categories.platform

Marketing analytics dashboard & lead scoring (Phase 5)

Analytics dashboard with marketing funnel visualization, lead scoring engine (100-point algorithm across 7 factors), churn detection via volume decline analysis, campaign performance tracking, and score distribution. Includes daily scheduled jobs for funnel snapshots and lead score recalculation, Inertia deferred props for fast page load, and comprehensive test coverage (28 backend + 6 frontend tests).

changelog.types.feature
changelog.categories.platform

Landing pages, case studies & lead capture (Phase 4)

Extends Article model with type field (ARTICLE, LANDING_PAGE, CASE_STUDY) for SEO-optimized landing pages and case studies. Includes public landing page rendering with embedded lead capture forms, lightweight lead capture endpoint with UTM tracking, admin lead management (list, detail, status workflow, assignment), newsletter auto-subscribe on consent, and blog admin type selector. 62 tests (42 backend + 20 frontend).

changelog.types.feature
changelog.categories.platform

PSP match notification & smart re-engagement (Phase 3)

Smart re-engagement system that matches merchants with new PSPs based on their payment interests. Includes merchant payment interest widget (dashboard), PSP match scoring engine (method groups, industry, volume), admin preview and bulk notification, email notifications via PspMatchMail, anti-spam protection, and KYB auto-enrichment. 54 tests (32 backend + 22 frontend).

changelog.types.feature
changelog.categories.platform

Lifecycle drip campaigns & email templates (Phase 2)

Automated lifecycle drip campaigns with multi-step email sequences triggered by merchant status changes. Includes drip campaign CRUD with step editor, email template management with variable substitution, merchant enrollment lifecycle (active/paused/cancelled/completed), conditional step execution (skip_if_status/require_status), campaign metrics and completion rates, scheduled daily processing, and auto-enrollment via MerchantStatusChanged event. 85 tests (50 backend + 35 frontend).

changelog.types.feature
changelog.categories.platform

Email marketing & newsletter system (Phase 1)

Complete email marketing platform with newsletter subscribers, email campaigns, email templates, and campaign recipient tracking. Includes admin CRUD for campaigns/subscribers, public newsletter subscription with double opt-in, unsubscribe flow, preference center, blog widget integration, and comprehensive test coverage (98 backend + 22 frontend + 55 E2E tests).

changelog.types.improvement
changelog.categories.platform

AI Assistant quick questions marquee animation

Replaced JS step-by-step carousel (setInterval + drag/swipe) with CSS marquee animation for Quick Questions in AI chat widget. Smooth continuous scroll, hover-to-pause, edge fade effect, prefers-reduced-motion support.

changelog.types.improvement
changelog.categories.security

Merchants must configure own AI API key

Removed system default LLM fallback for merchant portal. Merchants now must configure their own AI API key (OpenAI, Gemini, or Claude) to use the AI Assistant. Widget shows clear setup guide with CTA when unconfigured. Admin portal unchanged (uses system provider).

changelog.types.feature
changelog.categories.fraud

AI-powered fraud analysis tools for chat assistants

Added 6 fraud analysis tools to AI agents: 3 merchant tools (fraud summary with decision breakdown/risk distribution/daily trends, blacklist overview, rule effectiveness with trigger analysis) and 3 admin tools (platform-wide fraud overview, global fraud signal intelligence, learned rule performance). Merchant agent now has 10 tools, admin agent has 7. Includes ScopeValidator fraud keyword support. 28 new tests.

changelog.types.feature
changelog.categories.platform

SSE streaming responses for AI assistants

Added Server-Sent Events (SSE) streaming to both merchant and admin AI chat endpoints. Responses now appear progressively in real-time instead of waiting 3-10 seconds for blocking completion. Cache hits return JSON instantly. Includes new useStreamingChat composable with debounced markdown rendering, fallback to blocking on stream failure, and blinking cursor UX during streaming. 15 backend + 20 frontend tests.

changelog.types.feature
changelog.categories.platform

Agent + Tool Calling architecture for AI assistants

Replaced Two-Pass LLM architecture (parameter extraction → context building → answer generation) with Laravel AI SDK Agent + Tool Calling. Merchant agent has 8 tools (revenue, transactions, refunds, disputes, settlements, fraud, balance, compare), admin agent has 4 tools (platform overview, merchant ranking, PSP performance, financials). Includes 3 middleware (input sanitization, prompt leakage protection, token tracking). All 12 providers use agent path. 34 new tests with 103 assertions.

changelog.types.improvement
changelog.categories.platform

Replace LlmProviderFactory with Laravel AI SDK-based service

Replaced manual HTTP provider calls (752 LOC) with LlmSdkProviderService using Laravel AI SDK. All 12 LLM providers route through the SDK's unified API, including Together AI and Perplexity via OpenAI-compatible driver. CircuitBreaker integration preserved.

changelog.types.improvement
changelog.categories.platform

Remove Two-Pass legacy code and unsupported providers

Removed HuggingFace and AWS Bedrock providers (no SDK support), Two-Pass LLM architecture (parameter extraction + context building + answer generation), and all related code: LlmProviderFactory, MerchantDataContextBuilder, AdminDataContextBuilder, ParameterValidator, prompt template system. Reduced from 14 to 12 providers, all using Agent + Tool Calling. ~1,500 LOC removed.

changelog.types.feature
changelog.categories.api

Add conversion rate and total transactions to Payment Methods API

Payment Methods API now returns conversion_rate (success percentage) and total_transactions for each payment method, calculated from the merchant's last 30 days of payment attempt data. Cached for 5 minutes per merchant to minimize DB load.

changelog.types.feature
changelog.categories.api

Add preferredPaymentMethod parameter to transaction creation APIs

All 3 transaction creation endpoints (standard, iframe, S2S) now accept an optional preferredPaymentMethod parameter. When specified, the preferred method is tried first in the cascade order. For S2S, silently falls back to default gateway if the preferred method doesn't support S2S. Includes audit trail via payload_checkout.

changelog.types.feature
changelog.categories.fraud

Transaction alert indicators with detail dialog and severity filter

Added alert indicator column (first column) to admin transactions DataTable showing fraud and website mismatch warnings per transaction. Clicking an indicator opens a dialog with risk score bar, decision badges, and alerts grouped by category (geographic, website, fraud rules, risk score, blacklist, velocity, card testing, AML, browser, manual review). Added severity-based filter (Has Alerts, Critical, High, Medium, No Alerts) with URL persistence. Includes 31 backend tests, 23 frontend tests, and 10 E2E tests.

changelog.types.improvement
changelog.categories.fraud

Deferred fraud scoring and Redis-based frequency counting

Two-phase fraud evaluation defers 8 scoring-only checks for clean transactions (~95%) via Laravel defer(), reducing latency by 40-80ms. Redis atomic counters replace DB COUNT queries for calendar-window frequency rules, cutting per-rule check time from 10-30ms to 1-5ms. Falls back to DB automatically when Redis is unavailable.

changelog.types.improvement
changelog.categories.fraud

Cache fraud detection and payment routing for faster transactions

Added configurable caching layer across 6 services (FrequencyChecker, BlacklistChecker, FraudDetectionService, TransactionExpirationService, CascadingPaymentService, PaymentRoutingService) reducing DB queries from 31-53 to ~10-20 per transaction. Added FRAUD_CACHE_STORE config, early exit on blacklist block, batch country validation, and DB indexes for fraud queries.

changelog.types.fix
changelog.categories.platform

Fix alert service bugs and add comprehensive unit tests

Fixed 3 production bugs in alert notification pipeline: undefined array key access in Telegram/Slack services, void return type preventing sendTestAlert from returning results, and deduplication never recording fingerprints. Added 44 unit tests for TelegramAlertService, SlackAlertService, and AlertDispatcher.

changelog.types.improvement
changelog.categories.payment

Country-dependent billing state combobox and postal code hints

Billing state field now shows a searchable combobox with states/provinces for supported countries. Users can select from the list or type custom values. Postal code placeholder dynamically shows format hints based on selected country.

changelog.types.feature
changelog.categories.fraud

Amount-based velocity limits

Frequency rules can now limit by total transaction amount (e.g., block if IP exceeds $10,000/day) in addition to transaction count

changelog.types.feature
changelog.categories.fraud

Calendar-based time windows

Added calendar hour/day/week/month windows that reset at natural boundaries (midnight, start of week/month) instead of rolling periods

changelog.types.fix
changelog.categories.platform

Fix false-positive financial integrity alerts for missing REFUND fund flows

FinancialIntegrityVerifier was generating false HIGH alerts for 15,000+ completed refunds due to morph map mismatch: the Relation::morphMap short alias ('refund') was added without a data migration, so getMorphClass() returned 'refund' while existing fund_flows rows stored 'App\\Models\\Refund'. Fixed verifier to accept both formats via whereIn. Added chunked migration (SKIP LOCKED) to normalize non-Transaction rows. Fixed TransactionFactory::withPartialRefund()/withFullRefund() to create fund flows via FundFlowService. Added backfill:refund-fund-flows command for future gaps.

changelog.types.fix
changelog.categories.platform

Admin real-time updates now authorize correctly

Admin broadcasting channels (transaction monitoring and AI-agent session updates) resolved against the merchant web guard instead of the admin guard, so an authenticated operator was silently rejected and never received live updates. The admin channels now resolve the admin guard, restoring real-time updates on the operator portal.

Payment Links, browser anti-spoofing, and developer experience improvements

changelog.types.feature
changelog.categories.payment

Payment Links

Merchants can create shareable payment links for no-code payment collection with 6 customer fields and customer selector

changelog.types.feature
changelog.categories.security

Browser details collection & anti-spoofing detection

Collect browser fingerprint details and detect spoofed user agents for fraud prevention

changelog.types.improvement
changelog.categories.platform

Vue MCP debugging integration

Integrated vite-plugin-vue-mcp for AI-assisted Vue component debugging

changelog.types.improvement
changelog.categories.platform

Setup command improvements

Improved /setup command with APP_SERVICE check, seeder fallback, and hosts validation

Exchange rate markups, rolling reserves, and infrastructure modernization

changelog.types.feature
changelog.categories.payment

Exchange rate markup management

Configure exchange rate markups for currency conversion with merchant-specific rates

changelog.types.feature
changelog.categories.settlement

Rolling reserve management

Complete rolling reserve system with merchant filters, export, and integration tests

changelog.types.feature
changelog.categories.dashboard

Landing page redesign

Redesigned landing page with enhanced visuals, animations, and live checkout component

changelog.types.feature
changelog.categories.platform

A/B testing system

Checkout optimization through A/B testing with variant support and i18n

changelog.types.feature
changelog.categories.platform

Status page system

Public status page with admin management, live indicators, and API documentation

changelog.types.improvement
changelog.categories.platform

Docker service modernization

Renamed Docker service from laravel.test to epayse.app with auto-detect LAN IP

changelog.types.improvement
changelog.categories.dashboard

Mobile scroll-snap blog slider

Horizontal scroll-snap slider for blog cards on mobile with sticky stack cards

changelog.types.fix
changelog.categories.dashboard

FX Markups mobile tab overlap

Resolved mobile tab overlap on FX Markups page and DataTable pagination

Issue reports, admin permissions overhaul, and partner program foundation

changelog.types.feature
changelog.categories.dashboard

Issue reports

Merchants can report errors directly from the dashboard with admin notification

changelog.types.feature
changelog.categories.partner

Partner program foundation

Multi-tier referral system with commission tracking, tier-based rates, and downline management

changelog.types.improvement
changelog.categories.platform

Redis operation service

Standardized cache layer with RedisOperationService for consistent cache operations

changelog.types.fix
changelog.categories.security

Admin permission mismatches

Resolved 4 admin permission mismatches causing 403 errors

PSP management, fund flows, and manual payment reconciliation

Documentation portal, customer management, and API improvements

S2S payment API, security hardening, and checkout improvements

Sandbox system, dispute management, and blog platform

Initial release of EPaySe Payment Gateway Platform