Skip to main content

Refund Management

Source: internal/api/v1/store/Controllers/refund.js

Overview

The Refund controller manages the complete refund request lifecycle including creation, approval workflows, Stripe refund processing, and application fee refunds for connected accounts. It implements a multi-stage approval process for platform products and handles retry logic for failed refunds.

Key Capabilities

  • Create refund requests for invoice line items
  • Multi-stage approval workflow (account → provider)
  • Automatic Stripe refund processing on approval
  • Application fee refund handling
  • Retry failed refunds
  • Requester/requestee access control

MongoDB Collections

CollectionOperationsPurpose
_store.refundsCRUDRefund request tracking
_store.invoicesReadInvoice validation
_store.productsReadPlatform product detection, fee calculation
_store.pricesReadPrice and reference price lookup

Service Methods

newRefundRequest

Creates a refund request for invoice line items.

Endpoint: POST /store/refunds

Request Body:

{
invoice: string, // Invoice MongoDB _id
lines: [{
id: string, // Line item ID or product ID
quantity: number, // Quantity to refund
amount: number // OPTIONAL: refund amount (full line item only)
}]
}

Response: Created refund request object

Business Logic:

  1. Validate invoice access via generateInvoiceLookupOptions()
  2. Check for existing open refund requests for same invoice/items
  3. Validate line item IDs match invoice
  4. Validate quantities don't exceed invoice quantities
  5. Detect DashClicks platform products
  6. Flag application fee refunds
  7. Calculate refund amounts
  8. Create refund request with approval flags

DashClicks Product Detection:

const dcProducts = await StoreProduct.find({
stripe_id: { $in: product_ids },
$or: [
{ platform_type: 'dashclicks' },
{
$and: [{ platform_type: 'default' }, { account: parent_account_id }],
},
],
});

if (dcProducts?.length) {
refundRequest.provider_approval_required = true;
refundRequest.provider_approval_status = 'pending';
}

Application Fee Flagging:

dcProducts.forEach(product => {
refundRequest.lines.forEach(line => {
if (line.product === product.stripe_id && product.platform_type === 'default') {
line.application_fee_refund = true;
line.application_fee_amount = reference_price.unit_amount;
}
line.refund_amount = unit_amount * quantity;
});
});

getRefundRequest

Retrieves a single refund request by ID.

Endpoint: GET /store/refunds/:id

Authorization: Must be requester or requestee

Response: Refund request with populated invoice, requester, requestee


listRefundRequests

Lists refund requests with optional type filtering.

Endpoint: GET /store/refunds

Query Parameters:

{
page: number,
limit: number,
type: 'requester' | 'requestee', // Optional filter
filter: object // Optional: additional filters
}

Response: Paginated refund request list


refundRequestAction

Executes refund request actions (approve, deny, cancel).

Endpoint: POST /store/refunds/:id/actions/:action

Supported Actions: approve, deny, cancel

Business Logic:

APPROVE Action:

  1. Check approval requirements (account vs provider)
  2. Create Stripe refunds if both approvals granted
  3. Create application fee refunds if applicable
  4. Set status to 'completed' or 'approved' (with errors)
  5. Update completed_on timestamp

Stripe Refund Creation:

// Standard refund
const amount = lines.reduce((sum, line) => sum + line.refund_amount, 0);
const refund = await stripe.refunds.create(
{
charge: invoice.charge,
reason: 'requested_by_customer',
amount,
},
{ stripeAccount: connected_account },
);

// Application fee refund
const feeAmount = lines.reduce(
(sum, line) => (line.application_fee_refund ? sum + line.application_fee_amount : sum),
0,
);
const charge = await stripe.charges.retrieve(invoice.charge, { stripeAccount });
const appFeeRefund = await stripe.applicationFees.createRefund(charge.application_fee, {
amount: feeAmount,
});

DENY Action:

  • Sets status = 'denied'
  • Sets account_approval_status = 'denied'
  • Sets denied_by_requestee = true

CANCEL Action:

  • Only requester can cancel
  • Sets status = 'cancelled'
  • Sets cancelled_by_requester = true

Retry Logic:

If has_errors = true and action is 'approve', retries failed refunds:

for (refund of refundRequest.stripe_refunds) {
if (refund.object === 'error') {
if (refund.type === 'STRIPE_REFUND') {
const tmpRefund = await stripe.refunds.create(
refund.retry_data.request_body,
refund.retry_data.request_account,
);
refund = tmpRefund; // Replace error with success
}
}
}

Approval Workflow

Two-Stage Approval

graph TD
A[Create Refund Request] --> B{DashClicks Product?}
B -->|No| C[account_approval_required only]
B -->|Yes| D[provider_approval_required + account_approval_required]
C --> E[Account Approves]
D --> F[Provider Approves]
F --> G[Account Approves]
E --> H[Process Stripe Refunds]
G --> H
H --> I{Errors?}
I -->|No| J[Status: completed]
I -->|Yes| K[Status: approved, has_errors: true]

Approval States:

  • pending: Awaiting approval
  • approved: Approved at this level
  • denied: Denied at this level
  • cancelled: Cancelled by requester

Edge Cases & Business Rules

1. Full Line Item Refunds Only

Amount calculation commented out - only full line items supported:

// DISABLED AS ONLY ALLOWING FULL LINE ITEM REFUND
// Check if refund amount being requested is valid

2. Application Fee Refund Skipping

If standard refund fails, application fee refund is skipped:

if (stripe_data[0].object === 'error') {
stripe_data.push({
object: 'error',
type: 'STRIPE_APPLICATION_REFUND',
additional_info: 'Skipped because connected account refund failed',
});
}

3. Open Refund Prevention

Cannot create refund request if open request exists for same invoice/items:

const openRefundCheck = await StoreRefund.findOne({
requester: invoice.metadata.account_id,
'invoice.internal': invoice.id,
'lines.id': { $in: provided_line_ids },
status: { $nin: ['approved', 'denied'] },
});

4. Immutable States

Once refund is completed, denied, or cancelled, no further actions allowed.


💬

Documentation Assistant

Ask me anything about the docs

Hi! I'm your documentation assistant. Ask me anything about the docs!

I can help you with:
- Code examples
- Configuration details
- Troubleshooting
- Best practices

Try asking: How do I configure the API?
09:31 AM