changelog.title
changelog.subtitle
Fix AUTH-VULN-01: replace TRUSTED_PROXIES=* with auto-refreshed Cloudflare CIDRs
TRUSTED_PROXIES=* allowed any client to forge X-Forwarded-For and bypass IP-keyed rate limiters (login, 2FA, forgot-password). Implemented cloudflare:refresh-ips Artisan command that fetches Cloudflare public IP ranges daily and caches them (Cache::forever infrastructure:trusted_proxies). bootstrap/app.php now reads from cache with a priority chain: env override → cache → hardcoded fallback (fail-closed, never * in staging/production). Scheduled daily with withoutOverlapping(5). AWS_VPC_CIDR env var appends internal proxy network CIDR (required for Dokploy/Docker where Traefik IP is the REMOTE_ADDR, not CF IP directly).
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.
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.
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.
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.
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.
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.
Encrypt API key secrets and webhook tokens at rest (A.4)
Added Laravel `encrypted` cast to ApiKey.secret_key, ApiKey.previous_secret_keys (encrypted:array), and WebhookConfiguration.secret_token. Data migration widens columns to TEXT then encrypts all existing plaintext values via APP_KEY (AES-256-CBC). Prevents secret exposure if DB dump is leaked.
Post-Shannon R4 security expansion: 8 additional findings fixed
P0: PinddPayGateway webhook signature bypass — replaced AND logic (invalid-sig && failed-requery) with immediate rejection on invalid signature. P0/P1: AirWallexGateway and JPayGateway (deprecated) now throw RuntimeException on verifyWebhookSignature to prevent unverified webhook processing. P1: ArticleController bulk-action delete — moved checkAdminPermission outside try-catch so abort(403) propagates correctly instead of being swallowed as a redirect. P2: UpdateWebsiteRequest missing NoSsrfUrl rule — update path now matches store path SSRF + HTTPS enforcement. P2: Sandbox/PaymentForm.vue and CardPaymentForm.vue postMessage handlers missing origin check — added window.location.origin guard. P3: Deleted 3 unrouted dead-code methods in PaymentFormSimulatorController with unsafe transaction/refund ownership patterns. P3: Created config/security.php to wire up force_skip_ip_check and force_ssrf_validation env overrides previously referencing non-existent config file.
Fix 7 security findings from Shannon R4 pentest (P0-P2)
P0: Signed 3DS callback URLs (AUTHZ-VULN-04) — SandboxGateway now generates two pre-signed success/fail callback URLs; ThreeDSecureController rejects unsigned simulate param with 403. P0: SSRF guard on PSP credentials (SSRF-VULN-02) — NoSsrfUrl validation added to base_url/base_url_post/base_url_get in all three PSP create/update/credentials methods. P0: IDOR ownership check in PaymentFormSimulatorController — prevents cross-merchant API key exposure by verifying authenticated user's merchant matches requested merchantId. P1: Fixed postMessage origin allowlist logic in Checkout/Index.vue — empty allowedOrigins no longer silently passes all origins. P1: Fixed wrong config key subdomains.envs.→domains. in PaymentController allowedOrigins builder. P1: GenioPago verifyWebhookSignature throws RuntimeException (presence-only check was not real verification). P2: Removed 'staging' from HMAC IP-whitelist skip list; replace with config('security.force_skip_ip_check') flag for explicit overrides.
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.
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.
Patch Symfony CVEs (symfony/http-kernel, mailer, mime, routing) + fix ZAP CI pipeline
Upgraded 10 symfony/* packages to v8.0.12 to address 5 CVEs released 2026-05-20 (CVE-2025-XXXX series). Fixed ZAP Automation Framework CI: corrected script engine from ECMAScript→Groovy in api-v1.yaml; updated deprecated formBased→form and cookieBased→cookie method names in authenticated-merchant.yaml and authenticated-admin.yaml. Rewrote staging E2E resilient-request helper to use page.goto() (Chromium) instead of playwright.request (undici) to avoid 16KB response-header overflow from Vite's Link preload header. Updated security spec fixtures accordingly.
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.
OWASP ZAP Automation Framework — 5-layer security scanning
Replaces single-subdomain zap-baseline.py passive scan with 5 specialized CI jobs: passive (all 6 subdomains), active (public surface monthly), authenticated admin, authenticated merchant, HMAC-signed API v1. Adds ZapIntegrationTest.php with 10 permanent regression tests (SEC-101..SEC-110) mapping ZAP alert IDs to PHPUnit assertions. HmacSigner.groovy signs /api/v1/* requests after active-scan body mutations. Graduated blocking: advisory (Day 0-30) → passive blocks staging deploy (Day 31+) → API blocks (Day 61+). Removes critical /debug-500 route that leaked PHP version and env info.
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.
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.
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.
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.
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.
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.
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.
Patch HIGH severity vulnerabilities in phpoffice/phpspreadsheet (CVE-2026-34084 + 4 others)
Bumped phpoffice/phpspreadsheet from 1.30.2 to 1.30.4 (transitive dependency of maatwebsite/excel ^1.30.0). Fixes 5 advisories: CVE-2026-34084 SSRF/RCE in IOFactory::load (HIGH), CVE-2026-40902 + CVE-2026-40863 (HIGH), CVE-2026-40296 + CVE-2026-35453 XSS in HTML writer (medium). composer audit now clean. Also bumped 9 transitive dependencies (symfony/* 1.33→1.37, webmozart/assert 2.1→2.3, etc.) within minor version constraints — no breaking changes expected.
Prevent PSP circuit breaker resonance loop on mass-seeded transactions
MassTransactionSeeder PENDING transactions (5% x 10M = 500K) with fake provider_id triggered ProcessTransactionExpire every 5 min, calling real PinddPay -> 5 errors/60s tripped circuit breaker (300s cooldown ≈ 5min scheduler = resonance loop). Three-part fix: (1) MassDataGenerator removes PENDING from status distribution; (2) PinddPayGateway returns isNotFound flag for 404/NOT_FOUND responses, distinct from real PSP errors; (3) PspStatusQueryService bypasses recordCircuitFailure() when PSP confirms transaction not found. Added staging:cleanup-mass-seed-pending artisan command for stuck data cleanup.
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.
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.
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.
PCI-safe PSP payload sanitization in all gateways
sanitizeErrorBody() and sanitizePspArray() helpers moved to BaseGateway — all gateways now inherit PAN/CVV redaction for Log::* calls. mapDeclineCode() docblock mandates sanitization before logging PSP responses. Removed duplicate implementation from GenioPagoGateway.
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.
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.
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.
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).
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.
OFAC sanctioned countries hard block (non-disableable)
Sanctioned countries (CU, IR, KP, SY, BY, MM) are hard-blocked at the routing layer, active regardless of admin allowlist configuration or feature flag state. Admin UI rejects sanctioned countries in allowlist mode with AlertDispatcher critical alert and distinct AdminActivity audit event (payment_method.sanctioned_country_allow_attempt). Legal counsel sign-off required before changes per RB-006.
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.
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.
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.
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.
Redirect Proxy hardening — block page no longer leaks transaction metadata
Flagged bots now receive a generic Transaction Processed page with zero transaction metadata. Amount, currency, status, and description are no longer exposed to detected crawlers, satisfying the minimum-disclosure principle of the proxy layer.
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.
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.
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.
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.
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.
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.
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.
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.
Financial precision: gateway webhook chain — WebhookDTO + BaseGateway + all PSPs (sessions 9-10)
Closed final float precision leak in gateway webhook processing chain. Root cause: WebhookDTO::$amount was typed as float, causing PHP to coerce string amounts from PSP JSON payloads before they reached any bcmath code. Chain: PSP JSON → parseWebhookData() → WebhookDTO(float $amount) → BaseGateway::processPaymentAttempt(float $paidAmount) → payment_attempts.paid_amount (DB write). Fix: WebhookDTO::$amount: string, BaseGateway::processPaymentAttempt(string $paidAmount), all 10 gateway parser call sites use (string) cast. Also fixed: DuplicatePaymentAlertService float subtraction → bcmath abs+bccomp; ProcessSandboxWebhook (float) cast removed; IframeTemplateGateway/S2STemplateGateway/GenioPagoGateway (float) casts removed; AirWallexGateway/V2 round() → (string); JPayGateway/V2 round() → (string); PinddPay 4 violations including checkoutWebhook(float) type mismatch with base class. 48/48 tests pass.
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.
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.
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.
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.
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.
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.
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.
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.
Financial precision: float-to-string audit complete — 78+ violations fixed across 55 files
Sessions 7-12 of CAST-AS-TEXT audit. Expanded scope from ->sum() to float method signatures, native arithmetic operators, round(), and abs() patterns. (1) WebhookDTO::$amount: float → string — root of full precision chain: PSP JSON → WebhookDTO → BaseGateway::processPaymentAttempt(string) → paid_amount DB write. All 6 gateway parsers cast to string at parse site. (2) V1 gateway ghost violations — AirWallexGateway and JPayGateway createPayment() have own request_amount DB writes bypassing AbstractPaymentGateway base-class fix; JPayGateway/AirWallexGateway checkoutWebhook() also fixed. (3) ProtestFeeService::adjustProtestFee() — duplicated private float-arithmetic fee calculator replaced with bcmath; FundFlowService::createDisputeLossEntry(float) signature changed to string. (4) FundFlowController::store/manualPayout — (float) cast before createManualAdjustment() fund_flows DB write. (5) DisputeCreationService — chargebackAmount > 0 scalar comparison on bcmath string replaced with bccomp(). (6) BuyerClaimService, ManualPaymentAttemptService, BaseGateway::processRefund() — float signatures feeding DB writes. (7) InquirePendingTransactionStatus — (float) paid_amount before feeCalculator chain. (8) Test assertions updated for CAST AS TEXT string propagation to JSON responses. 424 tests pass.
Financial precision: PSP gateway refund status and 8 additional bcmath violations fixed
Session 6 CAST-AS-TEXT audit. (1) 5 PSP gateways (AirWallex, AirWallexV2, JPay, JPayV2, ExamplePSP) used epsilon comparison abs(paid-total)<0.01 for refund status — replaced with bccomp(paid_amount, totalRefunded, 2) === 0. Bug: epsilon incorrectly marks $99.99 vs $100.00 payment as REFUND_SUCCESS. bccomp at scale=2 gives correct REFUND_PARTIAL_SUCCESS. (2) Dashboard TransactionController — refunds.amount sum + float arithmetic for isAllowRefund determination replaced with CAST AS TEXT + bcsub chain + bccomp comparison. (3) GetPlatformFinancialsData AI tool — 3 missed violations (lines 51,57,64) from session 2 audit: transactions and refunds CASE-SUM now CAST AS TEXT; full bcsub/bcmul/bcdiv chain for net revenue and rate calculations. (4) StatementGenerationService — all collection and DB sums replaced with bcadd reduce + CAST AS TEXT; netSettlement now uses bcsub chain. 45/45 tests pass.
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.
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.
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).
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.
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.
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.
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.
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.
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.
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.
Sandbox transactions stuck PENDING — now correctly expired
ProcessTransactionExpire skipped sandbox transactions because PspStatusQueryService returned ERROR for SandboxGateway (unsupported). Added SandboxGateway branch returning notFound() result, which correctly allows expiration. Resolves 4,661 historical failed jobs.
PCI: Remove card data from browser console logs and fix postMessage targetOrigin
Removed console.log('Payment submitted:', formData) from Sandbox/PaymentForm.vue that exposed full PAN + CVV in browser.log. Fixed postMessage wildcard targetOrigin ('*') to window.location.origin in both PaymentForm.vue and AirWallex/DropInElement.vue to prevent card data interception by malicious parent frames.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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
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
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
Fix merchant statements page crash on empty currency config
Fixed dead code toArray() ?? fallback and added null safety guard for merchants without currency configurations
Fix CollectStatusMetrics missing Request parameter
Status metrics collection command failed after health monitoring auth was added, now creates internal authenticated request
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
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.
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.
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.
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.
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.
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.
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.
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.
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.
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).
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).
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.
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.
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.
Webhook Safety Guard — cross-transaction prevention
Defense-in-depth system preventing cross-transaction status updates from webhooks. PaymentIdentity VO, WebhookSafetyGuard runtime validator, auto-discovery GatewayRegistryTest (fails on missing contract tests), ReconcileTransactionStatusesCommand (daily reconciliation), legacy gateway fixes (JPay/PinddPay). 23 contract tests across 7 gateways, 527 gateway tests pass.
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).
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).
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).
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).
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).
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).
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.
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).
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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
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
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.
Payment Links, browser anti-spoofing, and developer experience improvements
Payment Links
Merchants can create shareable payment links for no-code payment collection with 6 customer fields and customer selector
Signed URL verification for Payment Links
Payment links now use signed URLs to prevent tampering and unauthorized access
Browser details collection & anti-spoofing detection
Collect browser fingerprint details and detect spoofed user agents for fraud prevention
Vue MCP debugging integration
Integrated vite-plugin-vue-mcp for AI-assisted Vue component debugging
Setup command improvements
Improved /setup command with APP_SERVICE check, seeder fallback, and hosts validation
Exchange rate markups, rolling reserves, and infrastructure modernization
Exchange rate markup management
Configure exchange rate markups for currency conversion with merchant-specific rates
Rolling reserve management
Complete rolling reserve system with merchant filters, export, and integration tests
Landing page redesign
Redesigned landing page with enhanced visuals, animations, and live checkout component
A/B testing system
Checkout optimization through A/B testing with variant support and i18n
Status page system
Public status page with admin management, live indicators, and API documentation
Docker service modernization
Renamed Docker service from laravel.test to epayse.app with auto-detect LAN IP
Mobile scroll-snap blog slider
Horizontal scroll-snap slider for blog cards on mobile with sticky stack cards
Session security tightening
Reduced session lifetime to 30min and remember cookie to 8 hours
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
Issue reports
Merchants can report errors directly from the dashboard with admin notification
Partner program foundation
Multi-tier referral system with commission tracking, tier-based rates, and downline management
Admin authorization overhaul
Added authorization checks to all admin controllers following OWASP A01 guidelines
Redis operation service
Standardized cache layer with RedisOperationService for consistent cache operations
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
