Skip to main content

Event Fund Claims

Author: Josh S. Sakweli, Backend Lead — QBIT SPARK CO LIMITED
Version: v1.0
Base URL: /api/v1/e-events/claims


Overview

When an event organizer sells tickets, revenue is held in escrow. A Fund Claim is a formal request to release a portion of that escrowed revenue into the organizer's wallet. Every claim goes through a PENDING → APPROVED / REJECTED lifecycle, and funds only move on explicit admin approval.

Authentication: All endpoints require Authorization: Bearer <jwt_token>


Business Rules

Rule Detail
One pending per event At most ONE PENDING claim per event at any time
Claimable amount formula totalRevenue − totalRefunded − totalClaimed − totalPendingClaims
Admin-only for active events If event has not ended, only ADMIN can initiate a claim
Refund deadline 3 days before event start. Past this → no refunds, organizer may claim early
Safe ratio (pre-deadline) Only up to 80% of net revenue claimable before deadline; 20% held as refund reserve
Escrow release Approval atomically releases escrow → organizer wallet. No partial releases
Admin claims require note adminNote is mandatory on admin-initiated claims
Refund safety Refunds and approvals use pessimistic write lock on EscrowAccount to prevent race conditions

Claim Status Lifecycle

Status Meaning
PENDING Submitted, awaiting admin review. Escrow NOT yet released
APPROVED Admin approved. Escrow atomically released to organizer wallet
REJECTED Admin rejected. No funds moved. Organizer may submit a new claim
CANCELLED Organizer cancelled before admin action. No funds moved

Standard Response Format

Success

{
  "success": true,
  "httpStatus": "OK",
  "message": "Fund claim submitted successfully",
  "action_time": "2026-04-27T10:30:45",
  "data": { }
}

Error

{
  "success": false,
  "httpStatus": "BAD_REQUEST",
  "message": "A pending claim already exists for this event",
  "action_time": "2026-04-27T10:30:45",
  "data": "A pending claim already exists for this event"
}

Endpoints


1. List All Claims (Admin)

Endpoint: GET /api/v1/e-events/claims
Access: 🔒 ROLE_SUPER_ADMIN or ROLE_STAFF_ADMIN
Purpose: Returns all fund claims across all events. Optionally filter by status.

Query Parameters:

Parameter Type Required Description Default
status ClaimStatus enum No Filter by: PENDING, APPROVED, REJECTED, CANCELLED — (all returned)

Success Response:

{
  "success": true,
  "httpStatus": "OK",
  "message": "Fund claims retrieved",
  "data": [
    {
      "claimId": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
      "claimNumber": "EFC-2026-000001",
      "eventId": "uuid",
      "eventTitle": "Dar Jazz Night",
      "eventStatus": "PUBLISHED",
      "organizerId": "uuid",
      "organizerName": "Amina Hassan",
      "status": "PENDING",
      "claimedAmount": 50000.00,
      "totalRevenueSnapshot": 80000.00,
      "totalRefundedSnapshot": 5000.00,
      "totalPreviouslyClaimedSnapshot": 0.00,
      "totalPendingAtSubmission": 0.00,
      "currency": "TZS",
      "adminInitiated": false,
      "adminId": null,
      "adminNote": null,
      "organizerNote": "Requesting first partial claim",
      "reviewedById": null,
      "reviewerName": null,
      "reviewNote": null,
      "reviewedAt": null,
      "escrowsReleasedCount": 0,
      "escrowsSkippedCount": 0,
      "actualReleasedAmount": null,
      "initiatedAt": "2026-04-20T08:00:00",
      "updatedAt": "2026-04-20T08:00:00"
    }
  ]
}

Response Fields:

Field Type Description
claimId UUID Unique claim identifier
claimNumber string Human-readable reference, e.g. EFC-2026-000001
eventId UUID The event this claim belongs to
eventTitle string Display name of the event
eventStatus EventStatus Current event status
organizerId UUID Organizer's account ID
organizerName string Organizer's display name
status ClaimStatus Current claim status
claimedAmount BigDecimal Amount the organizer requested
totalRevenueSnapshot BigDecimal Organizer's net revenue at time of submission
totalRefundedSnapshot BigDecimal Total refunds issued at time of submission
totalPreviouslyClaimedSnapshot BigDecimal Already released to wallet before this claim
totalPendingAtSubmission BigDecimal Other pending claim amounts at submission time
currency string Currency code, e.g. TZS
adminInitiated boolean true if an admin submitted on behalf of organizer
adminId UUID Admin's account ID (null if organizer-initiated)
adminNote string Admin's reason for initiating (null if organizer-initiated)
organizerNote string Optional note from organizer
reviewedById UUID ID of admin who reviewed (null if pending)
reviewerName string Name of reviewing admin (null if pending)
reviewNote string Admin's review note (null if pending)
reviewedAt LocalDateTime Timestamp of review (null if pending)
escrowsReleasedCount Integer Number of escrow entries released on approval
escrowsSkippedCount Integer Escrow entries skipped (e.g. already refunded)
actualReleasedAmount BigDecimal Actual amount credited to wallet on approval
initiatedAt LocalDateTime When the claim was submitted
updatedAt LocalDateTime Last updated timestamp

Possible Errors:

HTTP Status Description
401 UNAUTHORIZED Invalid or expired JWT
403 FORBIDDEN Caller does not have admin role

2. Get My Claims (Organizer)

Endpoint: GET /api/v1/e-events/claims/my-claims
Access: 🔒 Authenticated organizer (any role)
Purpose: Returns all fund claims submitted by the currently authenticated organizer, across all their events.

No query parameters.

Success Response:

{
  "success": true,
  "httpStatus": "OK",
  "message": "My fund claims retrieved",
  "data": [ /* array of EventFundClaimResponse — same structure as endpoint 1 */ ]
}

Possible Errors:

HTTP Status Description
401 UNAUTHORIZED Invalid or expired JWT

3. Get Claim by ID

Endpoint: GET /api/v1/e-events/claims/{claimId}
Access: 🔒 Authenticated — admin sees any claim; organizer sees only their own
Purpose: Returns full details of a single fund claim.

Path Parameters:

Parameter Type Required Description
claimId UUID Yes The claim's unique identifier

Success Response:

{
  "success": true,
  "httpStatus": "OK",
  "message": "Claim retrieved",
  "data": { /* EventFundClaimResponse */ }
}

Possible Errors:

HTTP Status Description
401 UNAUTHORIZED Invalid or expired JWT
403 FORBIDDEN Organizer attempting to view another organizer's claim
404 NOT_FOUND Claim with given ID does not exist

4. Get Event Claims

Endpoint: GET /api/v1/e-events/claims/event/{eventId}
Access: 🔒 Authenticated — organizer (own events) or admin (any event)
Purpose: Returns all claims ever submitted for a specific event.

Path Parameters:

Parameter Type Required Description
eventId UUID Yes The event's unique identifier

Success Response:

{
  "success": true,
  "httpStatus": "OK",
  "message": "Event claims retrieved",
  "data": [ /* array of EventFundClaimResponse */ ]
}

Possible Errors:

HTTP Status Description
401 UNAUTHORIZED Invalid or expired JWT
403 FORBIDDEN Organizer does not own this event
404 NOT_FOUND Event not found

5. Get Claimable Amount

Endpoint: GET /api/v1/e-events/claims/event/{eventId}/claimable-amount
Access: 🔒 Authenticated — organizer (own events) or admin
Purpose: Returns the current claimable amount breakdown for an event, including eligibility status and the active pending claim if one exists.

Path Parameters:

Parameter Type Required Description
eventId UUID Yes The event's unique identifier

Success Response:

{
  "success": true,
  "httpStatus": "OK",
  "message": "Claimable amount retrieved",
  "data": {
    "eventId": "uuid",
    "eventTitle": "Dar Jazz Night",
    "totalRevenue": 80000.00,
    "totalRefunded": 5000.00,
    "totalClaimed": 0.00,
    "totalPendingClaims": 0.00,
    "claimableAmount": 60000.00,
    "currency": "TZS",
    "eligible": true,
    "ineligibilityReason": null,
    "activePendingClaimId": null,
    "refundDeadline": "2026-05-10T00:00:00+03:00",
    "pastRefundDeadline": false
  }
}

Note on claimable amount: claimableAmount = totalRevenue − totalRefunded − totalClaimed − totalPendingClaims. Before the refund deadline, only 80% of net revenue is claimable. After the deadline, 100% is claimable.

Response Fields:

Field Type Description
totalRevenue BigDecimal Net organizer share of all non-refunded tickets
totalRefunded BigDecimal Gross amount returned to buyers
totalClaimed BigDecimal Already released to organizer wallet
totalPendingClaims BigDecimal Locked by current PENDING claims
claimableAmount BigDecimal What the organizer can claim right now
eligible boolean Whether a new claim can be submitted
ineligibilityReason string Human-readable reason if eligible = false
activePendingClaimId UUID ID of the existing pending claim, or null
refundDeadline ZonedDateTime eventStartDate − 3 days
pastRefundDeadline boolean true = refund window closed, full amount claimable

Possible Errors:

HTTP Status Description
401 UNAUTHORIZED Invalid or expired JWT
403 FORBIDDEN Organizer does not own this event
404 NOT_FOUND Event not found

6. Get Revenue Summary (Admin)

Endpoint: GET /api/v1/e-events/claims/event/{eventId}/revenue-summary
Access: 🔒 ROLE_SUPER_ADMIN or ROLE_STAFF_ADMIN
Purpose: Returns a full financial summary of an event including gross sales, platform fees, refunds, claimed amounts, and escrow balance. Used for admin audit and oversight.

Path Parameters:

Parameter Type Required Description
eventId UUID Yes The event's unique identifier

Success Response:

{
  "success": true,
  "httpStatus": "OK",
  "message": "Event revenue summary retrieved",
  "data": {
    "eventId": "uuid",
    "eventTitle": "Dar Jazz Night",
    "grossRevenue": 100000.00,
    "totalRefunded": 5000.00,
    "platformFees": 10000.00,
    "netOrganizerRevenue": 85000.00,
    "totalClaimed": 0.00,
    "totalPendingClaims": 0.00,
    "escrowBalance": 85000.00,
    "currency": "TZS"
  }
}

Possible Errors:

HTTP Status Description
401 UNAUTHORIZED Invalid or expired JWT
403 FORBIDDEN Caller does not have admin role
404 NOT_FOUND Event not found

7. Submit Claim (Organizer)

Endpoint: POST /api/v1/e-events/claims/event/{eventId}
Access: 🔒 Authenticated organizer — must own the event
Purpose: Organizer submits a fund claim for the full current claimable amount. The claim amount is computed server-side — organizer cannot override it.

Path Parameters:

Parameter Type Required Description
eventId UUID Yes The event to submit a claim for

Request Body (optional):

{
  "organizerNote": "Requesting first partial claim before event ends"
}

Request Body Fields:

Field Type Required Description Validation
organizerNote string No Optional note from the organizer Max 1000 characters

Guard Rules Applied Server-Side:

  • Event must belong to the authenticated organizer
  • Event must have ended OR pastRefundDeadline = true (otherwise only admin can claim)
  • claimableAmount must be > 0
  • No existing PENDING claim for this event

Success Response:

{
  "success": true,
  "httpStatus": "OK",
  "message": "Fund claim submitted successfully",
  "data": { /* EventFundClaimResponse with status: PENDING */ }
}

Possible Errors:

HTTP Status Description
400 BAD_REQUEST A pending claim already exists for this event
400 BAD_REQUEST Claimable amount is zero — nothing to claim
400 BAD_REQUEST Event has not ended and refund deadline has not passed — only admin can claim
401 UNAUTHORIZED Invalid or expired JWT
403 FORBIDDEN Authenticated user does not own this event
404 NOT_FOUND Event not found

8. Admin-Initiate Claim

Endpoint: POST /api/v1/e-events/claims/event/{eventId}/admin-initiate
Access: 🔒 ROLE_SUPER_ADMIN or ROLE_STAFF_ADMIN
Purpose: Admin submits a fund claim on behalf of the organizer. Used when the event is still active (ticket sales ongoing). The claim is flagged adminInitiated = true with a mandatory audit note.

Path Parameters:

Parameter Type Required Description
eventId UUID Yes The event to claim funds for

Request Body:

{
  "adminNote": "Organizer requested early release. Event ending tomorrow, no pending refunds."
}

Request Body Fields:

Field Type Required Description Validation
adminNote string Yes Mandatory reason for admin-initiated claim Not blank

Guard Rules Applied Server-Side:

  • claimableAmount must be > 0
  • No existing PENDING claim for this event
  • Claim is capped at claimableAmount (never full totalRevenue)

Success Response:

{
  "success": true,
  "httpStatus": "OK",
  "message": "Admin-initiated claim created",
  "data": {
    "claimId": "uuid",
    "claimNumber": "EFC-2026-000003",
    "status": "PENDING",
    "adminInitiated": true,
    "adminId": "uuid-of-admin",
    "adminNote": "Organizer requested early release. Event ending tomorrow, no pending refunds.",
    "claimedAmount": 64000.00,
    "currency": "TZS"
  }
}

Possible Errors:

HTTP Status Description
400 BAD_REQUEST A pending claim already exists for this event
400 BAD_REQUEST Claimable amount is zero — nothing to claim
401 UNAUTHORIZED Invalid or expired JWT
403 FORBIDDEN Caller does not have admin role
404 NOT_FOUND Event not found
422 UNPROCESSABLE_ENTITY adminNote is blank

9. Approve Claim (Admin)

Endpoint: POST /api/v1/e-events/claims/{claimId}/approve
Access: 🔒 ROLE_SUPER_ADMIN or ROLE_STAFF_ADMIN
Purpose: Admin approves a pending claim. This atomically acquires a pessimistic write lock on the EscrowAccount, validates available balance, releases escrow entries, and credits the organizer's wallet. The actualReleasedAmount may differ from claimedAmount if refunds were issued between submission and approval.

Path Parameters:

Parameter Type Required Description
claimId UUID Yes The claim to approve

Request Body (optional):

{
  "reviewNote": "Verified escrow balance. Approved for release."
}

Request Body Fields:

Field Type Required Description
reviewNote string No Optional note from reviewing admin

Critical: Approval acquires a PESSIMISTIC_WRITE lock on the EscrowAccount row. Concurrent refund operations for the same event will block until the approval transaction completes, preventing escrow from going negative.

Success Response:

{
  "success": true,
  "httpStatus": "OK",
  "message": "Fund claim approved and funds released",
  "data": {
    "claimId": "uuid",
    "claimNumber": "EFC-2026-000001",
    "status": "APPROVED",
    "claimedAmount": 50000.00,
    "actualReleasedAmount": 48000.00,
    "escrowsReleasedCount": 12,
    "escrowsSkippedCount": 1,
    "reviewedById": "admin-uuid",
    "reviewerName": "Admin John",
    "reviewNote": "Verified escrow balance. Approved for release.",
    "reviewedAt": "2026-04-27T14:30:00"
  }
}

Note on actualReleasedAmount: May be less than claimedAmount if one or more escrow entries were refunded between claim submission and admin approval. The escrowsSkippedCount indicates how many entries were skipped for this reason.

Possible Errors:

HTTP Status Description
400 BAD_REQUEST Claim is not in PENDING status
400 BAD_REQUEST Escrow balance insufficient to cover claim amount
401 UNAUTHORIZED Invalid or expired JWT
403 FORBIDDEN Caller does not have admin role
404 NOT_FOUND Claim not found

10. Reject Claim (Admin)

Endpoint: POST /api/v1/e-events/claims/{claimId}/reject
Access: 🔒 ROLE_SUPER_ADMIN or ROLE_STAFF_ADMIN
Purpose: Admin rejects a pending claim. No funds are moved. The organizer may submit a new claim after rejection.

Path Parameters:

Parameter Type Required Description
claimId UUID Yes The claim to reject

Request Body (optional):

{
  "reviewNote": "Pending dispute investigation. Please resubmit after resolution."
}

Request Body Fields:

Field Type Required Description
reviewNote string No Reason for rejection (shown to organizer)

Success Response:

{
  "success": true,
  "httpStatus": "OK",
  "message": "Fund claim rejected",
  "data": {
    "claimId": "uuid",
    "claimNumber": "EFC-2026-000001",
    "status": "REJECTED",
    "reviewedById": "admin-uuid",
    "reviewerName": "Admin John",
    "reviewNote": "Pending dispute investigation. Please resubmit after resolution.",
    "reviewedAt": "2026-04-27T14:35:00"
  }
}

Possible Errors:

HTTP Status Description
400 BAD_REQUEST Claim is not in PENDING status
401 UNAUTHORIZED Invalid or expired JWT
403 FORBIDDEN Caller does not have admin role
404 NOT_FOUND Claim not found

11. Cancel Claim (Organizer)

Endpoint: DELETE /api/v1/e-events/claims/{claimId}
Access: 🔒 Authenticated organizer — must be the claim owner
Purpose: Organizer cancels their own pending claim before any admin action. This frees up the pending amount, allowing a new claim to be submitted.

Path Parameters:

Parameter Type Required Description
claimId UUID Yes The claim to cancel

No request body.

Success Response:

{
  "success": true,
  "httpStatus": "OK",
  "message": "Fund claim cancelled",
  "data": null
}

Possible Errors:

HTTP Status Description
400 BAD_REQUEST Claim is not in PENDING status (already reviewed)
401 UNAUTHORIZED Invalid or expired JWT
403 FORBIDDEN Organizer does not own this claim
404 NOT_FOUND Claim not found

Financial Safety: Refund & Claim Concurrency

This section explains how the system prevents money from being over-released when a refund and a claim approval happen simultaneously.

The Race Condition Risk

Revenue = 10,000
PENDING claim = 10,000
→ Refund of 2,000 processed concurrently
→ Claim approved → releases 10,000
→ Refund also goes out → escrow goes negative 💀

Protections in Place

Layer Mechanism
Refund deadline No refunds allowed within 3 days of event. Eliminates risk entirely post-deadline.
Safe ratio Only 80% of net revenue claimable before deadline. 20% locked as refund reserve.
Pessimistic DB lock Both approveClaim() and processRefund() acquire PESSIMISTIC_WRITE lock on EscrowAccount row — serializing the two operations.
Balance check Before any deduction, service verifies escrow.balance >= amount. Throws InsufficientEscrowException if not.
Snapshot divergence actualReleasedAmount on APPROVED claims may be less than claimedAmount if escrow entries were refunded between submission and approval.

Safe Claimable Amount Formula

Before refund deadline:
  claimableAmount = (totalRevenue × 0.80) − totalRefunded − totalClaimed − totalPendingClaims

After refund deadline:
  claimableAmount = totalRevenue − totalRefunded − totalClaimed − totalPendingClaims

Endpoint Quick Reference

# Method Path Access Description
1 GET /claims Admin List all claims (filter by status)
2 GET /claims/my-claims Organizer Get my own claims
3 GET /claims/{claimId} Organizer / Admin Get single claim by ID
4 GET /claims/event/{eventId} Organizer / Admin Get all claims for an event
5 GET /claims/event/{eventId}/claimable-amount Organizer / Admin Get claimable amount breakdown
6 GET /claims/event/{eventId}/revenue-summary Admin Full event revenue summary
7 POST /claims/event/{eventId} Organizer Submit a new claim
8 POST /claims/event/{eventId}/admin-initiate Admin Admin-initiate claim for active event
9 POST /claims/{claimId}/approve Admin Approve claim + release escrow
10 POST /claims/{claimId}/reject Admin Reject claim
11 DELETE /claims/{claimId} Organizer Cancel pending claim

Standard Error Reference

HTTP Status Scenario
400 BAD_REQUEST Business rule violation (duplicate pending, zero claimable, wrong status)
401 UNAUTHORIZED Missing, expired, or malformed JWT
403 FORBIDDEN Insufficient role or ownership violation
404 NOT_FOUND Event or claim does not exist
422 UNPROCESSABLE_ENTITY Validation failure (e.g. blank adminNote)
500 INTERNAL_SERVER_ERROR Unexpected server error