medusa-plugin-approval
Adds cart approvals and spending limits to Medusa v2, so a buying company can require sign-off before an order is placed and cap what each employee spends.
Requires @evolve-framework/medusa-plugin-company:
approval settings belong to a company, and there is nowhere else for them to
hang. That is the only dependency. Everything else here works against Medusa's
own cart module.
When to use
Install it in a Medusa backend for a B2B storefront whose companies need
approvals or spending limits. It needs @evolve-framework/medusa-plugin-company
installed too. A shop without companies does not need it.
Get started
Install the package with the peer dependencies it needs. It comes from the Evolve registry, which needs a one-time sign-in.
pnpm add @evolve-framework/medusa-plugin-approval @evolve-framework/medusa-plugin-company @medusajs/cli @medusajs/framework @medusajs/medusaRequirements5
node>=20@evolve-framework/medusa-plugin-company*@medusajs/cli2.21.2@medusajs/framework2.21.2@medusajs/medusa2.21.2
For example:
// medusa-config.ts// After adding the plugins, run `npx medusa db:migrate` to create their tables.import { defineConfig } from "@medusajs/framework/utils";
module.exports = defineConfig({ // projectConfig, modules and other plugins as before plugins: [ { resolve: "@evolve-framework/medusa-plugin-company", options: {} }, { resolve: "@evolve-framework/medusa-plugin-approval", options: {} }, ],});What it does
A company can require sign-off before an order may be placed: from one of its
own administrators (admin), from the merchant (sales_manager), or both. The
storefront submits a cart for approval, and the plugin creates the approvals
the company's settings require. A cart under review is frozen: it cannot be
added to, changed or completed. An approver signing off on a basket that can
still change underneath them is not approving anything.
One rejection rejects the cart, whoever said it. Anything still pending keeps it pending. Only when every required decision is in and none was a rejection is the cart approved. A rejected cart can never be completed.
Nobody approves their own request. Without that rule an administrator's own spending would pass through a control that only ever asked them.
Spending limits
Each employee has a limit; the period it resets over is set on their company:
never, daily, weekly, monthly or yearly. Spend is counted from orders
already placed inside that window, so "600 a month" means what it says rather
than "600 per order". The limit is checked on cart completion, which is the last
point before an order exists. A limit of 0 means no limit, so a company can
add someone before it sets their budget.
Routes
Store, for the buying company. All of them need a signed-in customer:
POST /store/carts/:id/approvals |
submit a cart for approval |
GET /store/approvals |
approvals within the caller's company |
POST /store/approvals/:id |
approve or reject, as a company administrator |
GET/POST /store/approval-settings |
what the caller's company requires; changing it needs a company administrator |
Admin, for the merchant: GET /admin/approvals and POST /admin/approvals/:id,
limited to sales_manager approvals. The buying company's internal chain of
command is not the merchant's to override.
Like Medusa's own admin routes, these accept an admin session, a bearer token or a secret API key sent with HTTP Basic authentication. An approval handled with an API key records the key's id as the one who handled it.
Settings live under /store/approval-settings rather than under the company's
own path, so this plugin owns every route it authenticates. Routes hung off
another plugin's prefix would be unprotected wherever that plugin is absent.
Attribution
Ported from the Medusa B2B Starter (MIT), which is the reference for the three models, the cart links and the validation hooks.
The spending-limit check is rewritten, not ported. The starter's version
asks Query for employee.spending_limit alone and then reads
customer.employee.company and customer.orders off the result. Neither was
requested, so the reset period falls back to its default and past spend is
always zero. The limit silently degrades into a per-order cap, and a buyer with
a monthly budget can spend it repeatedly. Here every field is requested by name,
and order totals are named rather than covered by a wildcard, which is what an
order's computed fields require.
Also different: a company's approval settings are created on first use rather
than by the company-creation workflow, so companies work without this plugin
installed; and the cart's summary row is linked as well as written, without
which every read of cart.approval_status comes back empty and the cart looks
unblocked.

