> ## Documentation Index
> Fetch the complete documentation index at: https://docs.nelkap.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Refund approval

> Hold a refund until a manager approves it, then refund or explain, however long the answer takes.

Some work can't go on without a person's decision. Here, a customer asks for a refund, and a manager must approve it first. The answer might come in five minutes or in two weeks.

With Nelkap, the run asks the manager, then waits. While it waits, nothing runs in your app. When the manager answers, the run goes on from where it stopped, with the answer in hand, and refunds the payment or tells the customer why not.

![A refundOrder run waiting on the key refund\_ord\_42, woken by a refund.reviewed event with the same key.](https://mintcdn.com/holotus/zu12qw3F64kRzbLL/images/refund-approval-light.png?fit=max\&auto=format\&n=zu12qw3F64kRzbLL\&q=85\&s=0458d02a18d63b59df6ebdc60da0a066)

## Write the workflow

```ts nelkap/workflows/refund-order.ts theme={"theme":{"light":"light-plus","dark":"dark-plus"}}
import { sendCustomerEmail, sendReviewRequest } from '../../lib/email';
import { refundPayment } from '../../lib/payments';
import { nelkap } from '../client';

type RefundRequest = { orderId: string; customerEmail: string; reason: string };

type Review = { approved: boolean; note: string };

export async function refundOrder(request: RefundRequest) {
  await nelkap.step.run('review request', () => sendReviewRequest(request));

  const review = await nelkap.step.wait<Review>(`refund_${request.orderId}`);

  if (!review.approved) {
    await nelkap.step.run('declined email', () =>
      sendCustomerEmail(request.customerEmail, 'refund-declined', review.note),
    );

    return;
  }

  await nelkap.step.run('refund', () => refundPayment(request.orderId, `refund_${request.orderId}`));

  await nelkap.step.run('refunded email', () => sendCustomerEmail(request.customerEmail, 'refund-done', review.note));
}
```

The workflow takes the refund request as its payload:

1. It emails a manager a link to a review page in your app.
2. It waits on the key `refund_<orderId>`. The call ends, and the run stays paused until an event arrives with that key.
3. When the event arrives, `step.wait` returns its payload: the manager's review.
4. If the manager declined, the customer gets an email with the manager's note. Otherwise, the payment is refunded, and the customer gets an email saying it's done.

Replace the functions from `lib` with your own. Here, `refundPayment` passes an idempotency key to your payment provider, so a refund is never made twice, even if a crash cuts the step off before it's saved.

## Start a run for each request

Register the workflow:

```ts nelkap/index.ts theme={"theme":{"light":"light-plus","dark":"dark-plus"}}
import { nelkap } from './client';
import { refundOrder } from './workflows/refund-order';

nelkap.registerWorkflows([{ name: 'refundOrder', handler: refundOrder }]);

export { nelkap, refundOrder };
```

Then start a run when a customer asks for a refund:

```ts theme={"theme":{"light":"light-plus","dark":"dark-plus"}}
await nelkap.run(refundOrder, { orderId: 'ord_42', customerEmail: 'ana@example.com', reason: 'Arrived damaged' });
```

## Send the manager's answer

Your review page posts the manager's decision to a route of your app. The route emits it as an event, with the key the run waits on:

```ts app/api/refunds/review/route.ts theme={"theme":{"light":"light-plus","dark":"dark-plus"}}
import { nelkap } from '@/nelkap';

export async function POST(request: Request) {
  const review: { orderId: string; approved: boolean; note: string } = await request.json();

  // Check here that the signed-in user may approve refunds.

  await nelkap.emit({
    event: 'refund.reviewed',
    payload: { approved: review.approved, note: review.note },
    wakeOn: `refund_${review.orderId}`,
  });

  return Response.json({});
}
```

The run waiting on `refund_ord_42` goes on right away. The key is built from the order, so the route doesn't need to know the run's ID.

An event only wakes the runs already waiting on its key. That's why the workflow asks for the review in the step just before the wait: the run is waiting long before the manager can answer.

## See it run

While the manager thinks it over, the run shows as waiting in your project's runs. If the customer withdraws the request, cancel the run from its page. A run can wait up to a year.

## Ideas to extend it

* Remind the manager: start a second workflow that sleeps a day, then emails them if the refund is still pending.
* Listen for `refund.reviewed` in another workflow, for example to keep a record of every decision.


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.