mar-pod-b2b / module-b2b-purchase-approval
Company purchase requests and approval workflow for Marpod B2B.
Package info
gitlab.com/mar-pod-b2b/module-b2b-purchase-approval
Type:magento2-module
pkg:composer/mar-pod-b2b/module-b2b-purchase-approval
Requires
- php: ^8.5
- magento/framework: ^103.0
- magento/module-catalog: ^104.0
- magento/module-customer: ^103.0
- magento/module-quote: ^101.2
- magento/module-sales: ^103.0
- magento/module-store: ^101.1
- mar-pod-b2b/module-b2b-bulk-add-to-cart: ^1.0
- mar-pod-b2b/module-b2b-company: ^1.0
- mar-pod-b2b/module-b2b-core: ^1.0
This package is auto-updated.
Last update: 2026-08-21 21:47:48 UTC
README
Company purchase requests and a configurable, multi-step approval workflow.
Status
Full service-layer engine shipped: purchase requests with a cart snapshot, amount-based
approval rules, sequential multi-step approval, and conversion back into a fresh cart on final
approval. A customer-facing GraphQL workflow (create/submit/approve/reject/cancel/convert-to-cart)
and an admin REST surface for rule-config management are also shipped — see REST and GraphQL
below. No Admin or storefront UI yet beyond that REST/GraphQL transport — Section 9.5
(Marpod_B2bAdvancedApproval) builds a full Admin UI and conditional rule groups on top of this
module's frozen contracts without modifying them.
Implemented foundation
- declarative schema:
marpod_b2b_purchase_request,marpod_b2b_purchase_request_item,marpod_b2b_approval_rule,marpod_b2b_approval_step,marpod_b2b_approval_history; PurchaseRequestStatusInterface::ALLOWED_TRANSITIONS— the single source of truth for the workflow:draft -> pending_approval -> approved -> ordered, withrejected/cancelled/expiredas terminal side branches frompending_approval;PurchaseRequestWorkflowInterface::transition()— the only supported way to change a request's status; validates the target against the allowed-transition map before writing;PurchaseRequestManagementInterface::createFromCart()— snapshots the logged-in customer's active cart into a newdraftrequest (one row per cart item: SKU, qty, price);submit()moves it topending_approvaland creates the required approval steps (count resolved byApprovalRuleEvaluatorInterface::getRequiredStepCount()againstmarpod_b2b_approval_ruleamount thresholds for the request's company);approveStep()/rejectStep()— advance or reject one pending step in sequence; a request only reachesapprovedonce every required step has been approved. Approval steps are strictly sequential: this module does not support parallel/any-of approval — seeMarpod_B2bAdvancedApprovalfor conditional rule groups built on top of the same primitives;cancel()— terminal transition available fromdraftorpending_approval;convertToCart()— turns anapprovedrequest back into cart line items for checkout;- every step is recorded in
marpod_b2b_approval_history(decision, actor, timestamp) —actorCustomerId = nullrepresents a system/automatic transition (e.g. auto-approval when no rule requires a step), the same convention used byMarpod_B2bQuote's workflow.
Approval rules are evaluated through the ApprovalRuleEvaluatorInterface pool so new condition
types (category, SKU, payment method, cost center) can be added later without changing the
core module — this is the extension point Marpod_B2bAdvancedApproval uses.
REST and GraphQL
Administrative REST routes below /rest/V1/marpod-b2b/purchase-approval require the
Marpod_B2bPurchaseApproval::manage ACL resource. They expose CRUD for a company's approval
rule configuration (amount_threshold rows) — merchant/admin configuration, since no
company-level permission exists for managing rules.
Authenticated customers get the full workflow through GraphQL: marpodB2bPurchaseRequest/
marpodB2bPurchaseRequests queries (a plain requester always sees their own request; seeing
another member's requires company.purchase_approval.view_company) and
marpodB2bCreatePurchaseRequestFromCart/marpodB2bSubmitPurchaseRequest/
marpodB2bApprovePurchaseRequestStep/marpodB2bRejectPurchaseRequestStep/
marpodB2bCancelPurchaseRequest/marpodB2bConvertPurchaseRequestToCart mutations. Every
mutation delegates to PurchaseRequestManagementInterface, which already fully enforces company
membership and the relevant permission — the resolvers never call
PurchaseRequestWorkflowInterface::transition() directly, since that primitive performs no
authorization at all.
Requirements
- PHP 8.5
- Magento 2.4.9-compatible framework packages
mar-pod-b2b/module-b2b-core,mar-pod-b2b/module-b2b-company,mar-pod-b2b/module-b2b-bulk-add-to-cart
Installation
Run from the Magento project root:
composer require mar-pod-b2b/module-b2b-purchase-approval:^1.1
bin/magento module:enable Marpod_B2bPurchaseApproval
bin/magento setup:upgrade
bin/magento setup:di:compile
bin/magento cache:clean
Development
The planned scope, dependency rules, cache boundaries, security requirements, and test strategy
are documented in packages/B2B_LIGHT_PLAN.md in the development Magento project.
Unit tests:
vendor/bin/phpunit -c dev/tests/unit/phpunit.xml.dist packages/module-b2b-purchase-approval/Test/Unit
REST/GraphQL API tests (real HTTP against a running instance — routing, authentication, ACL
enforcement, and cross-company isolation; see mar-pod-b2b/module-b2b-core's README for the
shared Test/Api base classes these build on):
vendor/bin/phpunit -c dev/tests/api-functional/phpunit_rest.xml \
packages/module-b2b-purchase-approval/Test/Api/Rest/ApprovalRuleConfigRestTest.php
vendor/bin/phpunit -c dev/tests/api-functional/phpunit_graphql.xml \
packages/module-b2b-purchase-approval/Test/Api/GraphQl/PurchaseRequestGraphQlTest.php
Limitations
- no Admin or storefront UI in this module — drive the workflow through REST/GraphQL or the
service contracts directly, or install
Marpod_B2bAdvancedApprovalfor an Admin UI and richer rule conditions; - approval is strictly sequential — no parallel ("any N of M") approval;
- no email notifications on submission, approval, or rejection.
Uninstallation
Do not remove the package while another installed module requires it (Marpod_B2bBudget,
Marpod_B2bAdvancedApproval, and Marpod_B2bRequisitionList all depend on it). The
marpod_b2b_purchase_request* and marpod_b2b_approval_* tables contain business data and are
retained when the module is disabled or its package is removed.