
Returns Refund Policy
- 86 installs
- 41 repo stars
- Updated March 13, 2026
- finsilabs/awesome-ecommerce-skills
Automate returns and refunds with configurable return windows, restocking fees, and rule-based approval per product type.
About
Configures return-window rules, restocking fees, and rule-based approval logic that varies by product type. A developer uses it to enforce a consistent, automated return and refund policy.
- Configurable return windows and restocking fees
- Rule-based approval logic per product type
Returns Refund Policy by the numbers
- 86 all-time installs (skills.sh)
- Ranked #865 of 2,715 Automation & Workflows skills by installs in the Skillselion catalog
- Data as of Aug 3, 2026 (Skillselion catalog sync)
npx skills add https://github.com/finsilabs/awesome-ecommerce-skills --skill returns-refund-policyAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 86 |
|---|---|
| repo stars | ★ 41 |
| Last updated | March 13, 2026 |
| Repository | finsilabs/awesome-ecommerce-skills ↗ |
What it does
Automate returns and refunds with configurable return windows, restocking fees, and rule-based approval per product type.
Files
Returns & Refund Policy Engine
Overview
A returns and refund policy engine enforces your rules automatically: different return windows per product category, restocking fees for specific item types, final-sale exclusions, and extended windows for loyalty members. This prevents your customer service team from manually evaluating every return request and ensures consistent policy enforcement. Most platforms can implement these rules through their returns apps combined with product tags and customer segments.
When to Use This Skill
- When your return logic is inconsistent because it's handled case-by-case by customer service
- When you need different return windows for different product categories (electronics vs. apparel vs. consumables)
- When implementing tiered return policies where loyalty members get extended windows or waived restocking fees
- When building an automated approval workflow that handles most returns without human intervention
- When compliance or legal requirements mandate that return policies be auditable and version-controlled
Core Instructions
Step 1: Determine your platform and choose the right returns tool
| Platform | Recommended Tool | Why |
|---|---|---|
| Shopify | Loop Returns or AfterShip Returns | Loop is the most feature-complete: supports per-product-type policies, restocking fees, final-sale blocking, and loyalty tier overrides |
| WooCommerce | ReturnGo or WooCommerce Returns and Warranty Requests | ReturnGo supports custom policy rules per product category and automated approval logic |
| BigCommerce | AfterShip Returns Center or Loop Returns | Both support per-category policy rules and restocking fees |
| Custom / Headless | Build a policy evaluation engine + Shippo for return labels | Store policies in a database; evaluate them programmatically when return requests come in |
Step 2: Define your return policy rules
Before configuring any tool, define your policy matrix clearly:
| Product Category | Return Window | Restocking Fee | Auto-Approve | Notes |
|---|---|---|---|---|
| Default (apparel, accessories) | 30 days from delivery | 0% | Yes | Most items |
| Electronics | 15 days from delivery | 15% | No — manual review | Opened electronics |
| Final Sale | 0 days | — | No | No returns |
| VIP / Gold members | 60 days from delivery | 0% | Yes | Override for loyalty tier |
| Defective / Wrong item | 90 days from delivery | 0% | Yes | Customer not at fault |
Step 3: Configure policy rules in your returns app
Shopify — Loop Returns
1. Install Loop Returns from the Shopify App Store 2. Go to Loop → Policy → Return Windows:
- Default: 30 days
- Click "Add Rule" → set "Product tag is 'electronics'" → return window: 15 days
- Click "Add Rule" → set "Product tag is 'final-sale'" → return window: 0 days (no returns)
3. Tag your products accordingly in Shopify admin (Products → Tags) 4. Go to Loop → Policy → Restocking Fees:
- Add a rule: "Product tag is 'electronics'" → restocking fee: 15%
5. Go to Loop → Policy → Customer Segments:
- Add a rule: "Customer tag is 'gold-member' or 'vip'" → return window override: 60 days, restocking fee: 0%
6. Enable auto-approval for eligible returns in Loop → Settings → Automation: "Auto-approve returns that meet policy conditions" 7. Loop generates the return label automatically when a return is approved
Testing your policy:
- Use Loop's Policy Simulator (Loop → Policy → Test Policy) to verify that a hypothetical return request (product type, customer tag, days since delivery) applies the correct rule
WooCommerce — ReturnGo
1. Install ReturnGo from retgo.com (or WordPress.org) 2. Go to ReturnGo → Return Policy:
- Set default return window: 30 days
- Under "Custom Rules", add product-category-based rules:
- Category "Electronics" → 15 days, 15% restocking fee, requires manual review
- Category "Final Sale" → 0 days (no returns allowed)
3. Under "Customer Rules": add tag-based overrides:
- Customer tag "wholesale" → 14 days, 10% restocking fee
4. Configure auto-approval: ReturnGo → Automation → enable "Auto-approve returns that match policy" 5. Test by creating a return request as a customer to verify rules apply correctly
Configuring final-sale in WooCommerce:
- Create a product category or tag called "final-sale"
- In ReturnGo, add a rule blocking returns for this category/tag
- On the product page, display the "Final Sale — No Returns" message using a product badge plugin
BigCommerce — AfterShip Returns Center
1. Install AfterShip Returns Center from the BigCommerce App Marketplace 2. Go to AfterShip → Policy:
- Set the default return window and configure exceptions by product type
3. AfterShip's policy engine supports return windows and auto-approval rules based on product tags 4. For restocking fees: AfterShip includes restocking fee configuration in their paid plans
Step 4: Handle return window calculation correctly
The single most important setting: the return window should start from the delivery date, not the order date or ship date.
Why this matters:
- Shipping a package takes 2–10 days depending on the service
- A 30-day return window starting from order date may leave the customer with only 20 days to actually return the item
- Most consumer protection laws (EU 14-day right of withdrawal, UK 14 days) count from delivery
Verify this in your returns app:
- Loop Returns: go to Loop → Settings → Return Window → "Starts from: Delivery Date" ✓
- ReturnGo: go to Settings → Policy → "Return window starts from: Delivered date" ✓
- AfterShip: go to Settings → Return Policy → "Start date: Order delivered date" ✓
If your returns app doesn't have tracking integration to detect delivery, use "Order Date + carrier average transit time" as an approximation.
Step 5: Communicate policies clearly
1. Returns page: Create a dedicated /returns or /return-policy page with your policy matrix in a table format — customers reference this before purchasing 2. Product pages: Show a brief returns statement near the "Add to Cart" button: "30-day free returns" or "Final Sale — No Returns" for final sale items 3. Order confirmation email: Include a link to your returns page and a brief "30-day returns" statement 4. Return window expiry reminder: Set up an automated email 7 days before a customer's return window closes — Loop and AfterShip both support this
Step 6: Custom / Headless — policy evaluation logic
// Return policy rules stored in database and evaluated programmatically
interface ReturnPolicy {
id: string;
name: string;
priority: number; // higher = evaluated first
conditions: {
productTags?: string[]; // match any of these tags
customerTags?: string[]; // match any of these customer tags
orderTags?: string[]; // e.g., ['final-sale']
};
windowDays: number; // 0 = no returns allowed
restockingFeePct: number; // 0–100
autoApprove: boolean;
}
async function evaluateReturnEligibility(params: {
orderId: string;
productId: string;
customerId: string;
returnReason: string;
deliveredAt: Date;
}): Promise<{
eligible: boolean;
policy: ReturnPolicy | null;
restockingFeeCents: number;
requiresManualReview: boolean;
daysRemaining: number;
reason?: string;
}> {
const order = await db.orders.findById(params.orderId);
const product = await db.products.findById(params.productId, { include: ['tags'] });
const customer = await db.customers.findById(params.customerId, { include: ['tags'] });
// Find highest-priority matching policy
const policies = await db.returnPolicies.findAll({ is_active: true }, { orderBy: ['priority', 'desc'] });
const policy = policies.find(p => {
const productMatch = !p.conditions.productTags?.length ||
p.conditions.productTags.some(tag => product.tags.includes(tag));
const customerMatch = !p.conditions.customerTags?.length ||
p.conditions.customerTags.some(tag => customer.tags.includes(tag));
const orderMatch = !p.conditions.orderTags?.length ||
p.conditions.orderTags.some(tag => order.tags?.includes(tag));
return productMatch && customerMatch && orderMatch;
}) ?? null;
if (!policy || policy.windowDays === 0) {
return { eligible: false, policy, restockingFeeCents: 0, requiresManualReview: false, daysRemaining: 0, reason: 'FINAL_SALE_OR_NO_POLICY' };
}
// Calculate days since delivery
const daysSinceDelivery = Math.floor((Date.now() - params.deliveredAt.getTime()) / 86400000);
const daysRemaining = policy.windowDays - daysSinceDelivery;
if (daysRemaining < 0) {
return { eligible: false, policy, restockingFeeCents: 0, requiresManualReview: false, daysRemaining: 0, reason: 'WINDOW_EXPIRED' };
}
// Calculate restocking fee on the item's original price
const orderLine = await db.orderLines.findOne({ order_id: params.orderId, product_id: params.productId });
const itemValueCents = orderLine.unit_price_cents * orderLine.quantity;
const restockingFeeCents = Math.round(itemValueCents * (policy.restockingFeePct / 100));
return {
eligible: true,
policy,
restockingFeeCents,
requiresManualReview: !policy.autoApprove,
daysRemaining,
};
}Best Practices
- Start the return window from delivery date, not order date — this is more fair to customers, reduces disputes, and aligns with consumer protection laws in most jurisdictions
- Version every policy change — when you update a return policy, log the old policy with a timestamp; apply the policy that was in effect at the time of purchase when a customer files a return
- Display restocking fees before the customer confirms the return — show "A 15% restocking fee ($12.75) will be deducted from your refund" during the return initiation flow, not after
- Cap auto-approval by refund value — even with auto-approval enabled, route returns over $500 to manual review to catch potential fraud
- Notify customers proactively about expiring windows — a "Your 30-day return window closes in 7 days" email for recent purchases reduces frustrated customers who missed the window
Common Pitfalls
| Problem | Solution |
|---|---|
| Return window calculated from order date instead of delivery date | Check your returns app settings explicitly for "return window starts from" — default in some tools is order date; change to delivery date |
| Multiple policies match and the wrong one applies | Sort by priority DESC and take the first match; document the priority hierarchy in your admin; test edge cases (VIP member buying electronics) |
| Customer disputes restocking fee | Show the fee amount and the policy name ("Electronics Policy — 15% restocking fee") in the return confirmation email so customers have documentation |
| Final sale tag not applied consistently | Create a process: every product added to a sale must have the "final-sale" tag applied; audit monthly using a product tag report |
Related Skills
- @returns-management
- @order-management-system
- @b2b-commerce
{
"context": "Tests whether the agent correctly implements the return processing workflow with the right status transitions, routes high-value returns to manual review regardless of auto_approve flag, logs eligibility evaluation calls, and proactively communicates restocking fees.",
"type": "weighted_checklist",
"checklist": [
{
"name": "Rejected status on ineligible",
"max_score": 8,
"description": "When a return request is not eligible, the system sets its status to 'rejected' and triggers a rejection notification (email or equivalent)"
},
{
"name": "Restocking fee stored before routing",
"max_score": 8,
"description": "The calculated restocking fee is persisted on the return request record before the request is routed to either auto-approval or manual review"
},
{
"name": "Manual review status 'requested'",
"max_score": 8,
"description": "When requiresManualReview is true, the return request status is set to 'requested' (not 'approved') and a notification is sent to the customer service team"
},
{
"name": "Auto-approved status 'approved'",
"max_score": 7,
"description": "When auto_approve is true (and the value cap doesn't trigger), the return request status is set to 'approved' and a shipping label is issued"
},
{
"name": "High-value cap routes to manual review",
"max_score": 12,
"description": "Return requests where the total item value exceeds a high-value threshold (e.g., $500 / 50000 cents) are routed to manual review even when the policy has auto_approve = true"
},
{
"name": "High-value cap does NOT reject",
"max_score": 9,
"description": "High-value returns that exceed the cap are routed to manual review (status 'requested'), NOT automatically rejected"
},
{
"name": "Eligibility call logging",
"max_score": 10,
"description": "Every call to the eligibility evaluation function is logged/stored with at minimum the input parameters (orderId, productIds, returnReason, customerId) and the resulting eligibility decision"
},
{
"name": "Restocking fee shown before confirmation",
"max_score": 9,
"description": "The return initiation flow returns or displays the calculated restocking fee amount to the customer before they confirm the return — the fee is not applied silently after confirmation"
},
{
"name": "Restocking fee policy source communicated",
"max_score": 8,
"description": "The restocking fee notification or confirmation includes the name or identifier of the policy from which the fee was derived"
},
{
"name": "Return window expiry notification",
"max_score": 9,
"description": "The implementation includes a mechanism (function, job, or script) to identify orders whose return window is expiring soon (within 7 days) and send a proactive notification"
},
{
"name": "Log includes policy result",
"max_score": 7,
"description": "The eligibility call log captures not just the input but also the outcome: which policy was matched and whether the return was approved, rejected, or routed to manual review"
},
{
"name": "Delivery date as return clock start",
"max_score": 5,
"description": "The return window calculation uses the delivery date (delivered_at) as day zero of the return window, not the order creation date or shipment date"
}
]
}
Automated Return Processing Workflow
Problem/Feature Description
FinSi processes several hundred return requests per day. Right now every request lands in a customer-service queue and a human has to open each ticket, look up the order, check whether it falls within the return window, and approve or reject it — a process that takes an average of 8 minutes per ticket and creates a 48-hour backlog during peak season. The operations director estimates that roughly 80% of requests are routine and could be handled without any human intervention.
The team also has a fraud concern: last month three fraudulent returns totalling over $2,000 slipped through because the original rules had no special handling for high-value items. They need the automated system to escalate anything above a certain threshold to the human team for a second look, even if the return would otherwise be auto-approved.
Customer satisfaction surveys highlight two recurring complaints: customers are surprised by restocking fees they weren't told about, and they feel blindsided when their return window quietly expires. The new system needs to address both.
Output Specification
Produce the following files:
workflow.ts(or.js) — The return request processing logic, including:- A
processReturnRequestfunction that evaluates eligibility and routes the request to the correct outcome, including special handling for high-value items - A
previewReturnfunction (or equivalent) that returns the calculated restocking fee and days remaining before the customer submits the final return request - A
sendExpiryNotificationsfunction (or equivalent job) that identifies orders whose return window closes within 7 days and queues a notification workflow.test.ts(or.js) — Unit tests covering:- An ineligible return is rejected and a rejection notification is triggered
- A low-value, auto-approve return is approved and a shipping label is issued
- A high-value return (above your chosen threshold) is routed to manual review even when the policy is configured for auto-approval
- The restocking fee is stored on the return request before the routing decision is made
- An eligibility evaluation call is recorded with its inputs and outcome
log-store.ts(or.js) — A simple in-memory or file-backed store for eligibility evaluation logs (used by the tests)
You may stub database and notification calls. Include a package.json if dependencies are needed. Tests should run with a standard npm test or npx jest invocation.
{
"context": "Tests whether the agent implements the core returns policy engine with the correct schema, priority-based policy resolution with NULL wildcard matching, proper reason codes in the eligibility result, and capped restocking fee calculation.",
"type": "weighted_checklist",
"checklist": [
{
"name": "return_policies table schema",
"max_score": 6,
"description": "The SQL schema defines a `return_policies` table with columns for product_category_ids (array), customer_segments (array), order_tags (array), return_window_days, restocking_fee_pct, restocking_fee_max, auto_approve, and is_active"
},
{
"name": "return_policy_versions table",
"max_score": 7,
"description": "A `return_policy_versions` table is defined with a `snapshot` column of JSONB type that stores a full policy snapshot and a foreign key to return_policies"
},
{
"name": "Default seed policies",
"max_score": 7,
"description": "At least 4 seed policies are inserted covering: a Final Sale / no-returns policy (0 days), an Electronics policy (15 days with restocking fee), an Apparel policy (30 days no fee), and a Standard fallback policy (30 days)"
},
{
"name": "Priority DESC ordering",
"max_score": 9,
"description": "Policy resolution fetches active policies ordered by priority descending and returns the first matching policy"
},
{
"name": "NULL wildcard matching",
"max_score": 9,
"description": "Policy resolution treats NULL/empty product_category_ids, customer_segments, or order_tags as matching all values (wildcard), not as requiring an empty array match"
},
{
"name": "NOT_DELIVERED reason code",
"max_score": 7,
"description": "Eligibility evaluation returns `eligible: false` with reason `NOT_DELIVERED` when the order status is not 'delivered'"
},
{
"name": "FINAL_SALE reason code",
"max_score": 8,
"description": "Eligibility evaluation returns `eligible: false` with reason `FINAL_SALE` when the matched policy has return_window_days of 0"
},
{
"name": "WINDOW_EXPIRED reason code",
"max_score": 8,
"description": "Eligibility evaluation returns `eligible: false` with reason `WINDOW_EXPIRED` when days since purchase exceeds return_window_days"
},
{
"name": "REASON_NOT_ALLOWED reason code",
"max_score": 8,
"description": "Eligibility evaluation returns `eligible: false` with reason `REASON_NOT_ALLOWED` when allowed_reasons is set and the return reason is not in the list"
},
{
"name": "daysRemaining in result",
"max_score": 6,
"description": "The eligibility result includes a `daysRemaining` field containing the number of days left in the return window"
},
{
"name": "Restocking fee percentage calculation",
"max_score": 9,
"description": "Restocking fee is calculated as round(itemValueCents * (restocking_fee_pct / 100))"
},
{
"name": "Restocking fee max cap",
"max_score": 9,
"description": "When restocking_fee_max is set, the calculated fee is capped at that maximum value (fee = min(calculatedFee, restocking_fee_max))"
},
{
"name": "requiresManualReview from auto_approve",
"max_score": 7,
"description": "The eligibility result's `requiresManualReview` field is set to the inverse of the matched policy's `auto_approve` flag"
}
]
}
Returns Policy Engine — Core Implementation
Problem/Feature Description
FinSi is an e-commerce platform that has grown rapidly and now handles dozens of product categories — everything from electronics to perishable consumables. Their current returns logic is hardcoded in a single customer-service script that treats every order identically. The operations team is constantly editing the script to handle exceptions: electronics need a shorter window and a restocking fee, final-sale clearance items shouldn't be returnable at all, and B2B wholesale accounts have different terms. Every change requires a developer deploy, and when customers dispute a return decision they have no record of which rule applied.
The team wants to replace this with a proper policy engine: a database-backed system where different rules can be configured per product category, customer segment, or order tag, without touching code. The engine must also tell callers exactly why a return was denied (window expired, final sale, reason not allowed, etc.) and what restocking fee to apply so the front end can show it to the customer before they confirm.
Output Specification
Produce the following files in your working directory:
schema.sql— DDL for all tables the policy engine requiresseed.sql— INSERT statements that populate a useful set of default policiespolicy-engine.ts(or.js) — The policy resolution and eligibility-evaluation logic, including:- A function that resolves the highest-priority matching policy for a given combination of product categories, customer segments, and order tags
- A function that evaluates whether a specific return request is eligible, returning a structured result object
- A function that calculates the restocking fee (as a cent amount) given a policy and the items' total value in cents
policy-engine.test.ts(or.js) — Unit tests covering at least the following cases:- A delivered order within the return window → eligible
- An order that has not yet been delivered → ineligible (with reason)
- An order past its return window → ineligible (with reason)
- A final-sale order → ineligible (with reason)
- A return reason not permitted by the applicable policy → ineligible (with reason)
- A high-restocking-fee scenario where the fee is capped at the maximum
- A policy with NULL category filter matching any product category
You may stub out any database calls. The test file should run with npx ts-jest or node --experimental-vm-modules node_modules/.bin/jest (or plain Node if you write .js). Include a package.json if needed.
{
"context": "Tests whether the agent implements policy versioning (immutable JSONB snapshots on every change), queries version history using the return_policy_versions table, and uses the policy active at purchase time for evaluation rather than the current policy.",
"type": "weighted_checklist",
"checklist": [
{
"name": "Version row on create",
"max_score": 10,
"description": "When a new policy is created, a corresponding row is inserted into `return_policy_versions` with a JSONB snapshot of the full policy"
},
{
"name": "Version row on update",
"max_score": 10,
"description": "When an existing policy is modified, a new row is inserted into `return_policy_versions` with a JSONB snapshot of the updated policy — the original row is not mutated"
},
{
"name": "Snapshot is full policy JSON",
"max_score": 9,
"description": "The `snapshot` column in `return_policy_versions` stores a complete JSONB representation of the policy at the time of the change, not just the changed fields"
},
{
"name": "changed_by recorded",
"max_score": 7,
"description": "Each version row records a `changed_by` value (user/actor ID) alongside the snapshot and timestamp"
},
{
"name": "Audit query by policy ID",
"max_score": 9,
"description": "A function or SQL query is provided that retrieves the version history for a specific policy, ordered by changed_at DESC"
},
{
"name": "Policy-at-purchase-time lookup",
"max_score": 12,
"description": "The implementation includes a function or query that retrieves the policy snapshot that was active at a specific timestamp (e.g., order.created_at) rather than the current policy"
},
{
"name": "Eligibility uses historical policy",
"max_score": 12,
"description": "The return eligibility evaluation uses the policy that was in effect at purchase time (not the current policy) when determining return window and fee"
},
{
"name": "Version table FK constraint",
"max_score": 8,
"description": "The `return_policy_versions` table has a foreign key referencing `return_policies(id)`"
},
{
"name": "Soft-delete, not hard-delete",
"max_score": 8,
"description": "Policies are deactivated via `is_active = false` (soft delete) rather than being removed from the table, preserving the foreign key chain for version history"
},
{
"name": "Test demonstrates version accumulation",
"max_score": 8,
"description": "A test or demonstration script shows that updating a policy twice results in two separate rows in `return_policy_versions` (i.e. history accumulates, not overwritten)"
},
{
"name": "Different decisions at different times",
"max_score": 7,
"description": "A test or example demonstrates that an order placed before a policy tightening (e.g., window reduced from 30 to 15 days) is still evaluated against the original 30-day window"
}
]
}
Returns Policy Audit Trail and Historical Evaluation
Problem/Feature Description
FinSi's legal team has flagged a compliance gap in the returns system: whenever a return is disputed, the customer service team has no way to prove what the return policy said at the time the customer placed the order. The company recently tightened the electronics return window from 30 days to 15 days, and several customers with older orders are now receiving incorrect rejection notices because the system is applying the new policy retroactively. One customer has already filed a chargeback, citing a policy they can prove was different when they bought.
The ops team needs two things: first, an immutable record of every change made to any return policy so they can reconstruct what rules were in force on any given date; second, the return eligibility engine must evaluate a return request against the policy that was active when the order was placed, not whatever rule currently exists.
Output Specification
Produce the following files:
schema.sql— DDL for areturn_policiestable and a versioning/audit table that captures the full state of a policy at each changepolicy-service.ts(or.js) — Business logic including:- A function to create a new policy (must record an initial version)
- A function to update an existing policy (must record a new version, not overwrite history)
- A function to retrieve the full version history for a policy, ordered most-recent-first
- A function to look up which policy snapshot was in effect at a given timestamp for a given product category / customer segment combination
- A return-eligibility evaluation function that uses the policy active at the order's purchase date
demo.ts(or.js) — A runnable script (no database required; use in-memory structures) that:
1. Creates a "Standard" 30-day return policy 2. Updates it to reduce the window to 15 days 3. Shows the version history (both versions should appear) 4. Evaluates return eligibility for an order placed before the change using the historical policy (should use 30-day window) 5. Evaluates return eligibility for an order placed after the change (should use 15-day window) 6. Prints a summary of each evaluation result
Run the demo with npx ts-node demo.ts or node demo.js.
{
"name": "finsi/returns-refund-policy",
"version": "0.1.0",
"summary": "Policy engine for return windows, restocking fees, and automated approvals",
"skills": {
"returns-refund-policy": {
"path": "SKILL.md"
}
}
}