Skip to content

medusa-plugin-approval

@evolve-framework/medusa-plugin-approval · v1.2.1

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

  1. 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/medusa
    Requirements5
    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.