ForhoppPay Connect — Disputes
Feature Reference: FR-29 | Service:
ConnectDisputeService
1. Overview
When a customer disputes a charge with their bank (chargeback), ForhoppPay creates a dispute record, holds the disputed funds from payouts, and notifies the merchant via webhook. Merchants can submit evidence through the dashboard. Disputes resolve as WON (merchant keeps funds) or LOST (funds returned to customer).
2. Dispute ID Format
dp_[a-zA-Z0-9]{32}
Example: dp_a1b2c3d4e5f6g7h8i9j0k1l2m3n4o5p6
3. Dispute Lifecycle
Customer files dispute
│
▼
┌────────┐
│ OPEN │ ← Funds held, merchant notified
└───┬────┘
│ Merchant submits evidence
▼
┌──────────────┐
│ UNDER_REVIEW │ ← Evidence being reviewed
└───┬──────┬───┘
│ │
▼ ▼
┌─────┐ ┌──────┐
│ WON │ │ LOST │
└─────┘ └──────┘
│ │
│ └─ Funds returned to customer
└─ Funds released to merchant
4. Dispute Statuses
| Status | Description | Funds | Terminal |
|---|---|---|---|
OPEN |
Dispute opened, evidence needed | Held | No |
UNDER_REVIEW |
Evidence submitted, being reviewed | Held | No |
WON |
Resolved in merchant's favor | Released to merchant | Yes |
LOST |
Resolved in customer's favor | Returned to customer | Yes |
5. Fund Holds
When a dispute is created:
- The charge status changes to
DISPUTED - The disputed amount is excluded from payout calculations
disputeRepository.sumActiveDisputedAmount(merchantAccountId)tracks total held amount
Active disputes (OPEN or UNDER_REVIEW) have their amounts held. The payout service deducts this from available balance:
Payout = Captured - Fees - Refunds - ActiveDisputedAmounts
6. Dispute Creation
6.1 Internal Flow
ConnectDisputeService.createDispute(chargeId, merchantAccountId, reason, disputedAmountCents)
6.2 Validation Rules
| Rule | Constraint |
|---|---|
| Charge status | Must be CAPTURED or PARTIALLY_REFUNDED |
| Existing dispute | Charge must not have an active dispute |
| Disputed amount | > 0 and ≤ charge amount |
| Default amount | Full charge amount if not specified |
6.3 Side Effects
- Charge status →
DISPUTED - Charge
disputed_attimestamp set - Charge
disputereference set charge.disputedwebhook dispatched
7. Evidence Submission
ConnectDisputeService.submitEvidence(disputeId, merchantAccountId, evidence)
Evidence is a Map<String, Object> that can include:
- Proof of delivery tracking numbers
- Customer communication records
- Terms of service references
- Product/service descriptions
- Shipping confirmation
Submitting evidence automatically moves the dispute from OPEN to UNDER_REVIEW.
8. Dispute Resolution
8.1 Merchant Wins
ConnectDisputeService.resolveWon(disputeId, merchantAccountId, resolutionDetails)
- Dispute status →
WON - Charge status →
CAPTURED(restored) - Funds released back to merchant's available balance
- Dispute fee ($15) refunded
8.2 Merchant Loses
ConnectDisputeService.resolveLost(disputeId, merchantAccountId, resolutionDetails)
- Dispute status →
LOST - Charge remains
DISPUTED - Funds returned to customer
- Dispute fee ($15) retained
9. Dispute Fee
$15.00 (1500 cents) per dispute
- Charged when dispute is opened
- Refunded if merchant wins
- Retained if merchant loses
10. Querying Disputes
| Method | Description |
|---|---|
getDispute(disputeId, merchantAccountId) |
Get single dispute |
listDisputes(merchantAccountId, pageable) |
List all disputes |
listDisputesByStatus(merchantAccountId, status, pageable) |
Filter by status |
getActiveDisputes(merchantAccountId) |
Get OPEN + UNDER_REVIEW |
getHeldDisputeAmount(merchantAccountId) |
Total held amount in cents |
hasActiveDispute(chargeId) |
Check if charge has active dispute |
11. Database Schema
CREATE TABLE forhopppay.merchant_disputes (
id BIGSERIAL PRIMARY KEY,
dispute_id VARCHAR(50) NOT NULL UNIQUE,
charge_id BIGINT NOT NULL REFERENCES forhopppay.merchant_charges(id),
merchant_account_id BIGINT NOT NULL REFERENCES forhopppay.forhopppay_accounts(id),
amount_cents BIGINT NOT NULL,
currency VARCHAR(3) NOT NULL DEFAULT 'USD',
reason VARCHAR(100),
status VARCHAR(30) NOT NULL DEFAULT 'OPEN'
CHECK (status IN ('OPEN', 'UNDER_REVIEW', 'WON', 'LOST')),
evidence_details JSONB,
resolution_details JSONB,
evidence_due_by TIMESTAMP,
evidence_submitted_at TIMESTAMP,
resolved_at TIMESTAMP,
created_at TIMESTAMP NOT NULL DEFAULT NOW(),
updated_at TIMESTAMP
);
CREATE INDEX idx_merchant_disputes_charge ON forhopppay.merchant_disputes(charge_id);
CREATE INDEX idx_merchant_disputes_merchant ON forhopppay.merchant_disputes(merchant_account_id);
CREATE INDEX idx_merchant_disputes_status ON forhopppay.merchant_disputes(status);
12. Webhook Events
| Event | When |
|---|---|
charge.disputed |
Dispute opened |
The webhook payload includes the full charge object with status: "disputed".
13. Correctness Properties
| # | Property | Validates |
|---|---|---|
| 38 | Disputed amount held until resolution | FR-29.4 |
| 39 | Status is one of {open, under_review, won, lost} | FR-29.3 |
Last Updated: March 2026
