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
| Collection | Operations | Purpose |
|---|---|---|
_store.refunds | CRUD | Refund request tracking |
_store.invoices | Read | Invoice validation |
_store.products | Read | Platform product detection, fee calculation |
_store.prices | Read | Price 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:
- Validate invoice access via
generateInvoiceLookupOptions() - Check for existing open refund requests for same invoice/items
- Validate line item IDs match invoice
- Validate quantities don't exceed invoice quantities
- Detect DashClicks platform products
- Flag application fee refunds
- Calculate refund amounts
- 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:
- Check approval requirements (account vs provider)
- Create Stripe refunds if both approvals granted
- Create application fee refunds if applicable
- Set status to 'completed' or 'approved' (with errors)
- Update
completed_ontimestamp
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 approvalapproved: Approved at this leveldenied: Denied at this levelcancelled: 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.