@mercurjs/docs 2.3.8-canary.3 → 2.3.8-canary.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/content/learn/architecture.mdx +166 -263
- package/content/platform/attribute/concepts/attribute-types.mdx +73 -46
- package/content/platform/attribute/concepts/global-vs-inline.mdx +54 -32
- package/content/platform/attribute/concepts/variant-axes.mdx +54 -32
- package/content/platform/attribute/guides/attach-attributes-to-a-product.mdx +96 -53
- package/content/platform/attribute/guides/create-a-variant-axis.mdx +106 -57
- package/content/platform/attribute/guides/create-an-attribute.mdx +105 -56
- package/content/platform/attribute/overview.mdx +37 -33
- package/content/platform/attribute/reference/data-models.mdx +53 -38
- package/content/platform/attribute/reference/events.mdx +47 -24
- package/content/platform/attribute/reference/links.mdx +98 -27
- package/content/platform/attribute/reference/service.mdx +71 -32
- package/content/platform/attribute/reference/workflows.mdx +158 -28
- package/content/platform/catalog/concepts/master-products.mdx +39 -37
- package/content/platform/catalog/concepts/product-seller-allowlist.mdx +56 -35
- package/content/platform/catalog/concepts/status-lifecycle.mdx +55 -49
- package/content/platform/catalog/concepts/variants-categories-collections.mdx +55 -35
- package/content/platform/catalog/guides/allowlist-stores.mdx +100 -37
- package/content/platform/catalog/guides/create-a-master-product.mdx +124 -48
- package/content/platform/catalog/guides/publish-or-reject-a-product.mdx +120 -55
- package/content/platform/catalog/overview.mdx +39 -35
- package/content/platform/catalog/reference/data-models.mdx +154 -43
- package/content/platform/catalog/reference/events.mdx +41 -25
- package/content/platform/catalog/reference/links.mdx +196 -28
- package/content/platform/catalog/reference/service.mdx +57 -35
- package/content/platform/catalog/reference/workflows.mdx +124 -30
- package/content/platform/commission/concepts/order-commission-lines.mdx +54 -52
- package/content/platform/commission/concepts/rule-matching.mdx +53 -63
- package/content/platform/commission/concepts/rules-and-rates.mdx +61 -59
- package/content/platform/commission/guides/batch-update-rules.mdx +69 -41
- package/content/platform/commission/guides/create-a-rate.mdx +101 -53
- package/content/platform/commission/guides/refresh-order-commission-lines.mdx +104 -35
- package/content/platform/commission/overview.mdx +43 -35
- package/content/platform/commission/reference/data-models.mdx +59 -45
- package/content/platform/commission/reference/events.mdx +28 -34
- package/content/platform/commission/reference/links.mdx +42 -33
- package/content/platform/commission/reference/service.mdx +103 -28
- package/content/platform/commission/reference/workflows.mdx +101 -27
- package/content/platform/offer/concepts/pricing-and-inventory.mdx +63 -49
- package/content/platform/offer/concepts/shipping.mdx +28 -29
- package/content/platform/offer/concepts/what-is-an-offer.mdx +48 -45
- package/content/platform/offer/guides/bulk-create-offers.mdx +97 -61
- package/content/platform/offer/guides/create-an-offer.mdx +112 -60
- package/content/platform/offer/guides/manage-offer-inventory.mdx +78 -38
- package/content/platform/offer/overview.mdx +39 -32
- package/content/platform/offer/reference/data-models.mdx +77 -47
- package/content/platform/offer/reference/events.mdx +44 -18
- package/content/platform/offer/reference/links.mdx +92 -40
- package/content/platform/offer/reference/service.mdx +66 -31
- package/content/platform/offer/reference/workflows.mdx +146 -37
- package/content/platform/order-group/concepts/computed-totals.mdx +40 -37
- package/content/platform/order-group/concepts/order-splitting.mdx +41 -39
- package/content/platform/order-group/concepts/the-order-group.mdx +39 -32
- package/content/platform/order-group/guides/list-order-groups.mdx +96 -56
- package/content/platform/order-group/guides/retrieve-an-order-group.mdx +93 -42
- package/content/platform/order-group/guides/split-a-cart.mdx +115 -40
- package/content/platform/order-group/overview.mdx +42 -32
- package/content/platform/order-group/reference/data-models.mdx +48 -31
- package/content/platform/order-group/reference/events.mdx +29 -17
- package/content/platform/order-group/reference/links.mdx +90 -29
- package/content/platform/order-group/reference/service.mdx +55 -31
- package/content/platform/order-group/reference/workflows.mdx +83 -24
- package/content/platform/payout/concepts/account-lifecycle.mdx +64 -67
- package/content/platform/payout/concepts/accounts-and-onboarding.mdx +50 -40
- package/content/platform/payout/concepts/payout-pipeline.mdx +70 -73
- package/content/platform/payout/guides/create-a-payout-account.mdx +65 -41
- package/content/platform/payout/guides/process-a-provider-webhook.mdx +84 -38
- package/content/platform/payout/guides/start-provider-onboarding.mdx +90 -46
- package/content/platform/payout/overview.mdx +43 -34
- package/content/platform/payout/reference/data-models.mdx +63 -36
- package/content/platform/payout/reference/events.mdx +56 -30
- package/content/platform/payout/reference/links.mdx +52 -22
- package/content/platform/payout/reference/service.mdx +72 -33
- package/content/platform/payout/reference/workflows.mdx +80 -19
- package/content/platform/product-edit/concepts/change-actions.mdx +52 -45
- package/content/platform/product-edit/concepts/change-pipeline.mdx +60 -52
- package/content/platform/product-edit/concepts/status-and-auto-confirm.mdx +43 -46
- package/content/platform/product-edit/guides/confirm-or-decline-a-change.mdx +104 -56
- package/content/platform/product-edit/guides/edit-a-product.mdx +126 -46
- package/content/platform/product-edit/guides/request-a-revision.mdx +99 -39
- package/content/platform/product-edit/overview.mdx +41 -34
- package/content/platform/product-edit/reference/data-models.mdx +73 -47
- package/content/platform/product-edit/reference/events.mdx +39 -29
- package/content/platform/product-edit/reference/links.mdx +32 -24
- package/content/platform/product-edit/reference/service.mdx +45 -26
- package/content/platform/product-edit/reference/workflows.mdx +173 -31
- package/content/platform/review/concepts/product-vs-seller-reviews.mdx +42 -41
- package/content/platform/review/concepts/ratings-and-moderation.mdx +45 -44
- package/content/platform/review/concepts/the-review-model.mdx +52 -45
- package/content/platform/review/guides/compute-aggregate-ratings.mdx +62 -37
- package/content/platform/review/guides/create-a-review.mdx +109 -39
- package/content/platform/review/guides/moderate-a-review.mdx +83 -37
- package/content/platform/review/guides/respond-to-a-review.mdx +85 -40
- package/content/platform/review/overview.mdx +43 -33
- package/content/platform/review/reference/data-models.mdx +41 -27
- package/content/platform/review/reference/events.mdx +55 -37
- package/content/platform/review/reference/links.mdx +80 -22
- package/content/platform/review/reference/service.mdx +61 -33
- package/content/platform/review/reference/workflows.mdx +80 -24
- package/content/platform/store/concepts/lifecycle.mdx +68 -44
- package/content/platform/store/concepts/store-entity.mdx +45 -27
- package/content/platform/store/concepts/team.mdx +56 -35
- package/content/platform/store/guides/create-a-store.mdx +102 -35
- package/content/platform/store/guides/manage-the-team.mdx +122 -32
- package/content/platform/store/guides/moderate-a-store.mdx +107 -38
- package/content/platform/store/overview.mdx +41 -34
- package/content/platform/store/reference/data-models.mdx +132 -57
- package/content/platform/store/reference/events.mdx +42 -17
- package/content/platform/store/reference/links.mdx +67 -49
- package/content/platform/store/reference/service.mdx +105 -33
- package/content/platform/store/reference/workflows.mdx +304 -24
- package/llms.txt +111 -111
- package/package.json +1 -1
|
@@ -1,99 +1,96 @@
|
|
|
1
1
|
---
|
|
2
|
-
title: "
|
|
2
|
+
title: "How a payout reaches a seller"
|
|
3
3
|
sidebarTitle: "The payout pipeline"
|
|
4
|
-
description: "
|
|
4
|
+
description: "What Mercur ships for payouts, what you trigger yourself, and how the provider interface fits in."
|
|
5
5
|
---
|
|
6
6
|
|
|
7
|
-
|
|
8
|
-
seller, and how the provider interface fits in.
|
|
7
|
+
The payout pipeline is the path from a placed order to money in a seller’s account. Mercur ships the building blocks: the workflows, the webhook route and subscriber, and the provider interface. It doesn’t ship the scheduler that decides when an order is paid out. You choose that moment and call `createPayoutWorkflow` yourself.
|
|
9
8
|
|
|
10
|
-
##
|
|
9
|
+
## What ships in `@mercurjs/core`
|
|
11
10
|
|
|
12
|
-
|
|
13
|
-
represented by the `Payout` data model (table `payout`, id prefix `pout`). Its
|
|
14
|
-
`amount` is the order total minus the order's commission lines, and it belongs to
|
|
15
|
-
the seller's `PayoutAccount`.
|
|
11
|
+
The core plugin gives you every piece that talks to the provider or writes payout records:
|
|
16
12
|
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
13
|
+
- **Workflows**: `createPayoutAccountWorkflow`, `createOnboardingWorkflow`, `createPayoutWorkflow`, and `processPayoutForWebhookWorkflow`
|
|
14
|
+
- **Webhook route**: `POST /hooks/payout` keeps the raw body and emits `payout.webhook_received` with a 5000 ms delay and 3 attempts
|
|
15
|
+
- **Webhook subscriber**: `payout-webhook` parses the event through the provider and runs `processPayoutForWebhookWorkflow`
|
|
16
|
+
- **Providers**: the built-in `system` provider, plus `@mercurjs/payout-stripe-connect` as a separate package
|
|
21
17
|
|
|
22
|
-
|
|
23
|
-
subscribers move each order from authorized payment to settled transfer with no
|
|
24
|
-
manual step.
|
|
18
|
+
## What you wire yourself
|
|
25
19
|
|
|
26
|
-
|
|
27
|
-
The payout **workflows** (`createPayoutWorkflow`, `createPayoutAccountWorkflow`,
|
|
28
|
-
`createOnboardingWorkflow`, `processPayoutForWebhookWorkflow`) and the provider
|
|
29
|
-
**webhook subscriber** ship in `@mercurjs/core`. The **scheduled jobs** that
|
|
30
|
-
drive capture and daily payout are wired up in your project (under
|
|
31
|
-
`apps/api/src/jobs`), along with the `order.capture_requested` and
|
|
32
|
-
`payout.requested` events they emit. The steps below describe that intended
|
|
33
|
-
pipeline and its integration points, not jobs bundled in the core plugin.
|
|
34
|
-
</Note>
|
|
20
|
+
No scheduled job, subscriber, or route in core calls `createPayoutWorkflow`. Core also has no `jobs` directory, so nothing captures authorized payments on a timer.
|
|
35
21
|
|
|
36
|
-
|
|
22
|
+
`@mercurjs/types` exports a `PayoutEvents` enum with `order.capture_requested`, `order.authorization_expired`, and `payout.requested`, but nothing emits these events or subscribes to them. The enum carries a TODO to move them to order events. Treat the names as reserved, not as hooks you can listen to.
|
|
37
23
|
|
|
38
|
-
|
|
39
|
-
payment is `authorized`, the seller has an `ACTIVE` payout account, the order
|
|
40
|
-
meets the required fulfillment status (default `fulfilled`), and no payout
|
|
41
|
-
exists yet. As the capture deadline nears (authorization window minus safety
|
|
42
|
-
buffer), it emits `order.capture_requested`. If the authorization already
|
|
43
|
-
expired, it emits `order.authorization_expired`.
|
|
24
|
+
The module options also describe a capture and payout schedule. `PayoutModuleOptions` and `PAYOUT_MODULE_OPTION_DEFAULTS` define them, and `getOptions()` returns them with defaults applied, but no core code reads them:
|
|
44
25
|
|
|
45
|
-
|
|
26
|
+
| Option | Default | Intended meaning |
|
|
27
|
+
| --- | --- | --- |
|
|
28
|
+
| `disabled` | `false` | Turn off capture checks and daily payouts |
|
|
29
|
+
| `authorizationWindowMs` | `604800000` (7 days) | How long an authorization stays valid |
|
|
30
|
+
| `sellerActionWindowMs` | `259200000` (72 hours) | How long a seller has to act on an order |
|
|
31
|
+
| `captureSafetyBufferMs` | `86400000` (24 hours) | Margin before authorization expiry |
|
|
32
|
+
| `requiredFulfillmentStatus` | `"fulfilled"` | Minimum fulfillment status for a payout |
|
|
46
33
|
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
34
|
+
<Tip>
|
|
35
|
+
Read these options in your own job with `container.resolve(MercurModules.PAYOUT).getOptions()`. That keeps your schedule configurable from `medusa-config.ts`.
|
|
36
|
+
</Tip>
|
|
50
37
|
|
|
51
|
-
##
|
|
38
|
+
## How createPayoutWorkflow computes the amount
|
|
52
39
|
|
|
53
|
-
|
|
54
|
-
`payout.requested` for each one. An order qualifies when its payment is captured,
|
|
55
|
-
no payout exists yet, and the seller's account is `ACTIVE`.
|
|
40
|
+
`createPayoutWorkflow` takes one `order_id` and pays the seller’s share of that order. It runs 4 stages:
|
|
56
41
|
|
|
57
|
-
|
|
42
|
+
1. Load the order with `total`, `currency_code`, `seller.payout_account`, items, and shipping methods
|
|
43
|
+
2. Load every `commission_line` for those items and shipping methods, then sum their `amount`
|
|
44
|
+
3. Call `createPayoutStep` with `order.total` minus the commission sum, in the order’s currency
|
|
45
|
+
4. Link the payout to the seller and run the `payoutCreated` hook with `order_id` and `payout_id`
|
|
58
46
|
|
|
59
|
-
|
|
60
|
-
which loads the order with its seller, payout account, and commission lines,
|
|
61
|
-
computes the seller's share, calls the provider to initiate the transfer, and
|
|
62
|
-
creates a `Payout` record linked to the seller.
|
|
47
|
+
For an order with a total of 47.00 and 4.70 in commission lines, the payout amount is 42.30. The workflow throws `Seller does not have a payout account` when the seller has none, and the module throws when the account isn’t `active`.
|
|
63
48
|
|
|
64
|
-
|
|
65
|
-
The order id is used as the payout's `idempotency_key`, so a re-emitted
|
|
66
|
-
`payout.requested` event never produces a duplicate transfer.
|
|
67
|
-
</Note>
|
|
49
|
+
The provider receives the order id as its idempotency key. With Stripe Connect, the order id also becomes the transfer’s `transfer_group`, and the seller id goes into its metadata.
|
|
68
50
|
|
|
69
|
-
|
|
51
|
+
<Warning>
|
|
52
|
+
`createPayoutWorkflow` doesn’t check payment capture, fulfillment status, or earlier payouts for the order. Each run creates a new `Payout` row. Put those checks in the code that triggers it.
|
|
53
|
+
</Warning>
|
|
70
54
|
|
|
71
|
-
|
|
72
|
-
module registers **exactly one** provider. Stripe Connect ships out of the box.
|
|
73
|
-
Any other processor implements the same four methods.
|
|
55
|
+
## Trigger payouts from your own code
|
|
74
56
|
|
|
75
|
-
|
|
76
|
-
| --- | --- |
|
|
77
|
-
| `createPayoutAccount` | Create the connected account with the provider |
|
|
78
|
-
| `createOnboarding` | Produce onboarding data (e.g. a Stripe onboarding link) |
|
|
79
|
-
| `createPayout` | Initiate a transfer to the seller |
|
|
80
|
-
| `getWebhookActionAndData` | Parse a raw webhook into a `PayoutWebhookResult` |
|
|
57
|
+
You decide when an order is ready. This subscriber pays out when Medusa emits `order.fulfillment_created`:
|
|
81
58
|
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
module. The same code path works for any provider.
|
|
86
|
-
</Tip>
|
|
59
|
+
```ts title="src/subscribers/payout-on-fulfillment.ts"
|
|
60
|
+
import type { SubscriberArgs, SubscriberConfig } from "@medusajs/framework"
|
|
61
|
+
import { createPayoutWorkflow } from "@mercurjs/core/workflows"
|
|
87
62
|
|
|
88
|
-
|
|
63
|
+
export default async function payoutOnFulfillment({
|
|
64
|
+
event,
|
|
65
|
+
container,
|
|
66
|
+
}: SubscriberArgs<{ order_id: string }>) {
|
|
67
|
+
await createPayoutWorkflow(container).run({
|
|
68
|
+
input: { order_id: event.data.order_id },
|
|
69
|
+
})
|
|
70
|
+
}
|
|
89
71
|
|
|
90
|
-
|
|
91
|
-
|
|
72
|
+
export const config: SubscriberConfig = {
|
|
73
|
+
event: "order.fulfillment_created",
|
|
74
|
+
}
|
|
75
|
+
```
|
|
92
76
|
|
|
93
|
-
|
|
77
|
+
This fires once per fulfillment, so a partially fulfilled order would be paid more than once. Before you ship it, add a guard that skips orders already paid and orders whose payment isn’t captured. A scheduled job in `src/jobs` that scans orders works the same way.
|
|
78
|
+
|
|
79
|
+
## The provider interface
|
|
80
|
+
|
|
81
|
+
Every external call goes through `IPayoutProvider` from `@mercurjs/types`. The module loads exactly one provider and throws `Payout module should be initialized with exactly one provider` otherwise.
|
|
82
|
+
|
|
83
|
+
| Method | Called by | Returns |
|
|
94
84
|
| --- | --- | --- |
|
|
95
|
-
| `
|
|
96
|
-
| `
|
|
97
|
-
| `
|
|
98
|
-
| `
|
|
99
|
-
|
|
85
|
+
| `createPayoutAccount` | `createPayoutAccount` | Provider `id`, optional `status`, `data` |
|
|
86
|
+
| `createOnboarding` | `createOnboarding` | `data`, such as a Stripe account link |
|
|
87
|
+
| `createPayout` | `createPayouts` | Optional `status`, `data` |
|
|
88
|
+
| `getWebhookActionAndData` | `getWebhookActionAndData` | A `PayoutWebhookResult` |
|
|
89
|
+
|
|
90
|
+
When you register no provider, the module loads `system`. It returns `active` accounts and `paid` payouts without moving money, which suits local development and tests. Register Stripe Connect in `medusa-config.ts` with the [Stripe Connect integration](/resources/integrations/stripe-connect).
|
|
91
|
+
|
|
92
|
+
## Related
|
|
93
|
+
|
|
94
|
+
- [Account lifecycle](/platform/payout/concepts/account-lifecycle) for the statuses the webhooks set
|
|
95
|
+
- [Workflows reference](/platform/payout/reference/workflows) for the `payoutCreated` hook
|
|
96
|
+
- [Order commission lines](/platform/commission/concepts/order-commission-lines) for where the commission sum comes from
|
|
@@ -1,59 +1,83 @@
|
|
|
1
1
|
---
|
|
2
|
-
title: "Create a payout account"
|
|
2
|
+
title: "Create a payout account for a seller"
|
|
3
3
|
sidebarTitle: "Create a payout account"
|
|
4
|
-
description: "
|
|
4
|
+
description: "Run createPayoutAccountWorkflow from your own route to connect a seller to the payout provider."
|
|
5
5
|
---
|
|
6
6
|
|
|
7
|
-
|
|
8
|
-
own server code. This is useful in an onboarding flow or a custom API route.
|
|
7
|
+
Create a seller’s payout account from server code, for example in an operator route or a custom signup flow. The Vendor API already does this for sellers at `POST /vendor/payout-accounts`.
|
|
9
8
|
|
|
10
|
-
|
|
11
|
-
calls the configured provider to create the connected account, and links the
|
|
12
|
-
account to the store. Run it from any place that has access to the Medusa
|
|
13
|
-
container.
|
|
9
|
+
## Before you begin
|
|
14
10
|
|
|
15
|
-
|
|
11
|
+
You need:
|
|
16
12
|
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
13
|
+
- A seller id, such as `sel_01JD8Q1234567`
|
|
14
|
+
- A payout provider. Without one, the `system` provider creates accounts that are `active` at once
|
|
15
|
+
- For Stripe Connect, `@mercurjs/payout-stripe-connect` registered on the payout module, as in the [Stripe Connect integration](/resources/integrations/stripe-connect)
|
|
20
16
|
|
|
21
|
-
|
|
22
|
-
const { result } = await createPayoutAccountWorkflow(req.scope).run({
|
|
23
|
-
input: {
|
|
24
|
-
seller_id: req.params.id,
|
|
25
|
-
},
|
|
26
|
-
})
|
|
17
|
+
## Create the account
|
|
27
18
|
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
19
|
+
<Steps>
|
|
20
|
+
<Step title="Add a route that runs the workflow">
|
|
21
|
+
Create an admin route that takes the seller id from the path and the provider data from the body:
|
|
22
|
+
|
|
23
|
+
```ts title="src/api/admin/payout-setup/[id]/route.ts"
|
|
24
|
+
import type {
|
|
25
|
+
AuthenticatedMedusaRequest,
|
|
26
|
+
MedusaResponse,
|
|
27
|
+
} from "@medusajs/framework/http"
|
|
28
|
+
import { createPayoutAccountWorkflow } from "@mercurjs/core/workflows"
|
|
29
|
+
|
|
30
|
+
type Body = { country: string }
|
|
31
|
+
|
|
32
|
+
export async function POST(
|
|
33
|
+
req: AuthenticatedMedusaRequest<Body>,
|
|
34
|
+
res: MedusaResponse
|
|
35
|
+
) {
|
|
36
|
+
const { result } = await createPayoutAccountWorkflow(req.scope).run({
|
|
37
|
+
input: {
|
|
38
|
+
seller_id: req.params.id,
|
|
39
|
+
data: { country: req.body.country },
|
|
40
|
+
},
|
|
41
|
+
})
|
|
42
|
+
|
|
43
|
+
res.status(201).json({ payout_account: { id: result.id } })
|
|
44
|
+
}
|
|
45
|
+
```
|
|
31
46
|
|
|
32
|
-
The
|
|
33
|
-
|
|
47
|
+
The workflow validates the seller has no account, creates it through the provider, and links it to the seller. The response returns only the id, because `result.data` holds the full provider object.
|
|
48
|
+
</Step>
|
|
49
|
+
<Step title="Call the route">
|
|
50
|
+
Send the seller’s country. Stripe Connect requires `country` and throws `"country" is required` without it:
|
|
34
51
|
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
52
|
+
```bash
|
|
53
|
+
curl -X POST http://localhost:9000/admin/payout-setup/sel_01JD8Q1234567 \
|
|
54
|
+
-H "Authorization: Bearer your_admin_token" \
|
|
55
|
+
-H "Content-Type: application/json" \
|
|
56
|
+
-d '{ "country": "US" }'
|
|
57
|
+
```
|
|
39
58
|
|
|
40
|
-
|
|
59
|
+
With Stripe Connect, the new account is `pending` and its `data` holds the Express account Stripe created.
|
|
60
|
+
</Step>
|
|
61
|
+
</Steps>
|
|
41
62
|
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
63
|
+
## Handle a seller that already has an account
|
|
64
|
+
|
|
65
|
+
`validateSellerPayoutAccountStep` runs first and throws a `MedusaError` of type `INVALID_DATA` with the message `Seller already has a payout account`. Medusa returns it as a `400`. To reuse the existing account instead, look it up through the seller link:
|
|
45
66
|
|
|
46
67
|
```ts
|
|
47
|
-
await
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
data: { business_type: "company" },
|
|
52
|
-
},
|
|
68
|
+
const { data: [seller] } = await query.graph({
|
|
69
|
+
entity: "seller",
|
|
70
|
+
fields: ["id", "payout_account.id", "payout_account.status"],
|
|
71
|
+
filters: { id: "sel_01JD8Q1234567" },
|
|
53
72
|
})
|
|
54
73
|
```
|
|
55
74
|
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
75
|
+
## What happens when the provider call fails
|
|
76
|
+
|
|
77
|
+
The module creates the `PayoutAccount` row before it calls the provider. If the provider throws, the module deletes that row and rethrows, so no orphaned account is left. The provider receives the new `pacc` id as `data.account_id` and as the default `context.idempotency_key`. Stripe Connect stores that id in the Stripe account’s `metadata.account_id`, which is how webhooks find the account later.
|
|
78
|
+
|
|
79
|
+
## Next steps
|
|
80
|
+
|
|
81
|
+
- [Start provider onboarding](/platform/payout/guides/start-provider-onboarding) so the seller can finish setup
|
|
82
|
+
- [Account lifecycle](/platform/payout/concepts/account-lifecycle) for how the account becomes `active`
|
|
83
|
+
- [Workflows reference](/platform/payout/reference/workflows) for the steps inside this workflow
|
|
@@ -1,38 +1,70 @@
|
|
|
1
1
|
---
|
|
2
|
-
title: "Process
|
|
2
|
+
title: "Process payout provider webhooks"
|
|
3
3
|
sidebarTitle: "Process a webhook"
|
|
4
|
-
description: "
|
|
4
|
+
description: "Route provider webhooks to Mercur so payout accounts and payouts pick up the provider's status changes."
|
|
5
5
|
---
|
|
6
6
|
|
|
7
|
-
|
|
8
|
-
status changes. Mercur already wires this up. A subscriber listens for
|
|
9
|
-
`payout.webhook_received` and drives the update workflow. Understanding the path
|
|
10
|
-
lets you emit the event yourself or extend the flow.
|
|
7
|
+
Connect your payout provider’s webhooks to Mercur so account and payout statuses follow the provider. You need this before any Stripe Connect account can become `active`.
|
|
11
8
|
|
|
12
|
-
##
|
|
9
|
+
## Before you begin
|
|
13
10
|
|
|
14
|
-
|
|
15
|
-
provider, then runs `processPayoutForWebhookWorkflow`:
|
|
11
|
+
You need:
|
|
16
12
|
|
|
17
|
-
|
|
18
|
-
|
|
13
|
+
- A payout provider registered on the payout module, such as Stripe Connect
|
|
14
|
+
- For Stripe Connect, a webhook signing secret set as the provider’s `webhookSecret` option
|
|
15
|
+
- The [Stripe CLI](https://docs.stripe.com/stripe-cli) for local testing
|
|
19
16
|
|
|
20
|
-
|
|
21
|
-
return
|
|
22
|
-
}
|
|
17
|
+
## Receive webhooks
|
|
23
18
|
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
19
|
+
Mercur ships the route, the subscriber, and the workflow. You point the provider at the route and check the result.
|
|
20
|
+
|
|
21
|
+
<Steps>
|
|
22
|
+
<Step title="Point the provider at /hooks/payout">
|
|
23
|
+
In production, add a Stripe webhook endpoint for events on connected accounts, with the URL `https://your_backend_url/hooks/payout` and the `account.updated` event. Locally, forward Connect events with the Stripe CLI:
|
|
24
|
+
|
|
25
|
+
```bash
|
|
26
|
+
stripe listen --events account.updated \
|
|
27
|
+
--forward-connect-to localhost:9000/hooks/payout
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
The CLI prints a signing secret. Use it as the provider’s `webhookSecret`.
|
|
31
|
+
</Step>
|
|
32
|
+
<Step title="Let the subscriber run the workflow">
|
|
33
|
+
The route emits `payout.webhook_received` with the body, raw body, and headers. It delays the event by 5000 ms and retries up to 3 times. The shipped `payout-webhook` subscriber then parses it and runs the workflow:
|
|
34
|
+
|
|
35
|
+
```ts title="src/subscribers/payout-webhook.ts (in @mercurjs/core)"
|
|
36
|
+
const processedEvent = await payoutService.getWebhookActionAndData(input)
|
|
37
|
+
|
|
38
|
+
if (!processedEvent.data) {
|
|
39
|
+
return
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
const wfEngine = container.resolve(Modules.WORKFLOW_ENGINE)
|
|
43
|
+
await wfEngine.run(processPayoutForWebhookWorkflowId, {
|
|
44
|
+
input: processedEvent,
|
|
45
|
+
})
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
Stripe Connect verifies the `stripe-signature` header against `webhookSecret`. A bad signature throws, and the event bus retries.
|
|
49
|
+
</Step>
|
|
50
|
+
<Step title="Check the account status">
|
|
51
|
+
Read the account to confirm the update landed:
|
|
52
|
+
|
|
53
|
+
```ts
|
|
54
|
+
const payoutService = container.resolve(MercurModules.PAYOUT)
|
|
27
55
|
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
56
|
+
const account = await payoutService.retrievePayoutAccount(
|
|
57
|
+
"pacc_01JD8Q1234567"
|
|
58
|
+
)
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
The `status` is `active`, `restricted`, or `rejected`, depending on what Stripe reported.
|
|
62
|
+
</Step>
|
|
63
|
+
</Steps>
|
|
31
64
|
|
|
32
65
|
## Run the workflow directly
|
|
33
66
|
|
|
34
|
-
|
|
35
|
-
`PayoutWebhookResult`:
|
|
67
|
+
`processPayoutForWebhookWorkflow` takes a `PayoutWebhookResult`, so you can apply a status change without a webhook, for example in a test or a backfill script:
|
|
36
68
|
|
|
37
69
|
```ts
|
|
38
70
|
import { processPayoutForWebhookWorkflow } from "@mercurjs/core/workflows"
|
|
@@ -40,35 +72,49 @@ import { processPayoutForWebhookWorkflow } from "@mercurjs/core/workflows"
|
|
|
40
72
|
await processPayoutForWebhookWorkflow(container).run({
|
|
41
73
|
input: {
|
|
42
74
|
action: "payout.paid",
|
|
43
|
-
data: { id: "
|
|
75
|
+
data: { id: "pout_01JD8Q1234567" },
|
|
44
76
|
},
|
|
45
77
|
})
|
|
46
78
|
```
|
|
47
79
|
|
|
48
|
-
The workflow
|
|
80
|
+
The workflow runs at most one step: the one that matches `action`. An `account.*` action needs a `pacc` id, and a `payout.*` action needs a `pout` id. A missing `data.id` or the `not_supported` action changes nothing.
|
|
81
|
+
|
|
82
|
+
## Map webhooks in your own provider
|
|
83
|
+
|
|
84
|
+
When you write a provider, `getWebhookActionAndData` decides what each webhook means. Return the action and the Mercur id, not the provider’s id:
|
|
85
|
+
|
|
86
|
+
```ts title="src/providers/acme-payouts/service.ts"
|
|
87
|
+
async getWebhookActionAndData(
|
|
88
|
+
payload: PayoutWebhookActionInput
|
|
89
|
+
): Promise<PayoutWebhookResult> {
|
|
90
|
+
const body = payload.data as { type: string; reference: string }
|
|
91
|
+
|
|
92
|
+
if (body.type === "transfer.settled") {
|
|
93
|
+
return { action: "payout.paid", data: { id: body.reference } }
|
|
94
|
+
}
|
|
49
95
|
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
| `payout.processing` / `payout.paid` / `payout.failed` / `payout.canceled` | Set payout status accordingly |
|
|
96
|
+
return { action: "not_supported" }
|
|
97
|
+
}
|
|
98
|
+
```
|
|
54
99
|
|
|
55
|
-
|
|
56
|
-
Actions the provider can't map return `not_supported` (or a missing `data.id`),
|
|
57
|
-
and the workflow makes no change. It is safe to hand it every event the provider
|
|
58
|
-
sends.
|
|
59
|
-
</Note>
|
|
100
|
+
Verify the provider’s signature here with `payload.rawData` and `payload.headers` before you trust the body. The route keeps the raw body for this purpose.
|
|
60
101
|
|
|
61
|
-
## Emit the event
|
|
102
|
+
## Emit the event from another route
|
|
62
103
|
|
|
63
|
-
|
|
64
|
-
`payout.webhook_received` with the raw payload and let the shipped subscriber
|
|
65
|
-
take over.
|
|
104
|
+
If your provider must post to a different URL, emit `payout.webhook_received` yourself with the same shape. The shipped subscriber picks it up:
|
|
66
105
|
|
|
67
106
|
```ts
|
|
68
107
|
const eventBus = container.resolve(Modules.EVENT_BUS)
|
|
69
108
|
|
|
70
109
|
await eventBus.emit({
|
|
71
110
|
name: "payout.webhook_received",
|
|
72
|
-
data: { rawData, headers
|
|
111
|
+
data: { data: req.body, rawData: req.rawBody, headers: req.headers },
|
|
73
112
|
})
|
|
74
113
|
```
|
|
114
|
+
|
|
115
|
+
Enable `preserveRawBody` on that route’s body parser, or signature checks will fail.
|
|
116
|
+
|
|
117
|
+
## Next steps
|
|
118
|
+
|
|
119
|
+
- [Account lifecycle](/platform/payout/concepts/account-lifecycle) for every action and the status it sets
|
|
120
|
+
- [Events reference](/platform/payout/reference/events) for the `payout.webhook_received` payload
|
|
@@ -1,50 +1,94 @@
|
|
|
1
1
|
---
|
|
2
|
-
title: "Start provider onboarding"
|
|
2
|
+
title: "Start provider onboarding for a seller"
|
|
3
3
|
sidebarTitle: "Start onboarding"
|
|
4
|
-
description: "
|
|
4
|
+
description: "Run createOnboardingWorkflow to get a provider onboarding link, such as a Stripe Connect account link, for a seller."
|
|
5
5
|
---
|
|
6
6
|
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
`
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
7
|
+
Create or refresh a seller’s onboarding link from server code, so they can finish the provider’s setup. The Vendor API already does this at `POST /vendor/payout-accounts/:id/onboarding`.
|
|
8
|
+
|
|
9
|
+
## Before you begin
|
|
10
|
+
|
|
11
|
+
You need:
|
|
12
|
+
|
|
13
|
+
- A payout account for the seller, created with [Create a payout account](/platform/payout/guides/create-a-payout-account)
|
|
14
|
+
- For Stripe Connect, a `refresh_url` and a `return_url` in your vendor app
|
|
15
|
+
|
|
16
|
+
## Create the onboarding link
|
|
17
|
+
|
|
18
|
+
<Steps>
|
|
19
|
+
<Step title="Find the seller's payout account">
|
|
20
|
+
Onboarding runs against a `pacc` id, not a seller id. Resolve it through the `seller_payout_account` link:
|
|
21
|
+
|
|
22
|
+
```ts title="src/api/admin/payout-setup/[id]/onboarding/helpers.ts"
|
|
23
|
+
import type { MedusaContainer } from "@medusajs/framework/types"
|
|
24
|
+
import { ContainerRegistrationKeys } from "@medusajs/framework/utils"
|
|
25
|
+
|
|
26
|
+
export async function getPayoutAccountId(
|
|
27
|
+
container: MedusaContainer,
|
|
28
|
+
sellerId: string
|
|
29
|
+
) {
|
|
30
|
+
const query = container.resolve(ContainerRegistrationKeys.QUERY)
|
|
31
|
+
const { data: [seller] } = await query.graph({
|
|
32
|
+
entity: "seller",
|
|
33
|
+
fields: ["payout_account.id"],
|
|
34
|
+
filters: { id: sellerId },
|
|
35
|
+
})
|
|
36
|
+
return seller?.payout_account?.id as string | undefined
|
|
37
|
+
}
|
|
38
|
+
```
|
|
39
|
+
</Step>
|
|
40
|
+
<Step title="Run the workflow from a route">
|
|
41
|
+
Pass the URLs in `data`. The module merges them with the account’s stored `data`, which carries the Stripe account `id`:
|
|
42
|
+
|
|
43
|
+
```ts title="src/api/admin/payout-setup/[id]/onboarding/route.ts"
|
|
44
|
+
import type { MedusaRequest, MedusaResponse } from "@medusajs/framework/http"
|
|
45
|
+
import { MedusaError } from "@medusajs/framework/utils"
|
|
46
|
+
import { createOnboardingWorkflow } from "@mercurjs/core/workflows"
|
|
47
|
+
import { getPayoutAccountId } from "./helpers"
|
|
48
|
+
|
|
49
|
+
export async function POST(req: MedusaRequest, res: MedusaResponse) {
|
|
50
|
+
const accountId = await getPayoutAccountId(req.scope, req.params.id)
|
|
51
|
+
if (!accountId) {
|
|
52
|
+
throw new MedusaError(MedusaError.Types.NOT_FOUND, "No payout account")
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
const { result } = await createOnboardingWorkflow(req.scope).run({
|
|
56
|
+
input: {
|
|
57
|
+
account_id: accountId,
|
|
58
|
+
data: {
|
|
59
|
+
refresh_url: "https://vendor.example.com/payouts/refresh",
|
|
60
|
+
return_url: "https://vendor.example.com/payouts",
|
|
61
|
+
},
|
|
62
|
+
},
|
|
63
|
+
})
|
|
64
|
+
|
|
65
|
+
res.json({ url: result.data?.url })
|
|
66
|
+
}
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
For Stripe Connect, `result.data` is the Stripe account link, and `url` is the page you redirect the seller to.
|
|
70
|
+
</Step>
|
|
71
|
+
</Steps>
|
|
72
|
+
|
|
73
|
+
## Refresh an expired link
|
|
74
|
+
|
|
75
|
+
Stripe account links expire. Call the same route again: the module finds the account’s existing `Onboarding` record and updates its `data` instead of creating a second one. Point `refresh_url` at a page in your vendor app that calls this route, so Stripe can send the seller back for a fresh link.
|
|
76
|
+
|
|
77
|
+
## Required data for Stripe Connect
|
|
78
|
+
|
|
79
|
+
The Stripe Connect provider checks 3 values and throws a `MedusaError` of type `INVALID_DATA` when one is missing:
|
|
80
|
+
|
|
81
|
+
- **`id`**: the Stripe account id, read from the payout account’s stored `data`
|
|
82
|
+
- **`refresh_url`**: where Stripe sends the seller when the link is no longer valid
|
|
83
|
+
- **`return_url`**: where Stripe sends the seller after they leave the flow
|
|
84
|
+
|
|
85
|
+
Put the URLs in `data`, not `context`. The module forwards `context` to the provider, but Stripe Connect reads the URLs only from `data`.
|
|
86
|
+
|
|
87
|
+
## The account becomes active by webhook
|
|
88
|
+
|
|
89
|
+
Finishing onboarding doesn’t change the account by itself. Stripe sends `account.updated`, the `payout-webhook` subscriber maps it to an action, and `processPayoutForWebhookWorkflow` sets the status.
|
|
90
|
+
|
|
91
|
+
## Next steps
|
|
92
|
+
|
|
93
|
+
- [Process a provider webhook](/platform/payout/guides/process-a-provider-webhook) to follow the status update
|
|
94
|
+
- [Account lifecycle](/platform/payout/concepts/account-lifecycle) for how Stripe updates map to statuses
|