mar-pod-b2b/module-b2b-purchase-approval

Company purchase requests and approval workflow for Marpod B2B.

Maintainers

Package info

gitlab.com/mar-pod-b2b/module-b2b-purchase-approval

Issues

Type:magento2-module

pkg:composer/mar-pod-b2b/module-b2b-purchase-approval

Transparency log

Statistics

Installs: 1

Dependents: 2

Suggesters: 0

Stars: 0

1.1.0 2026-08-17 17:08 UTC

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, with rejected/cancelled/ expired as terminal side branches from pending_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 new draft request (one row per cart item: SKU, qty, price); submit() moves it to pending_approval and creates the required approval steps (count resolved by ApprovalRuleEvaluatorInterface::getRequiredStepCount() against marpod_b2b_approval_rule amount thresholds for the request's company);
  • approveStep()/rejectStep() — advance or reject one pending step in sequence; a request only reaches approved once every required step has been approved. Approval steps are strictly sequential: this module does not support parallel/any-of approval — see Marpod_B2bAdvancedApproval for conditional rule groups built on top of the same primitives;
  • cancel() — terminal transition available from draft or pending_approval;
  • convertToCart() — turns an approved request back into cart line items for checkout;
  • every step is recorded in marpod_b2b_approval_history (decision, actor, timestamp) — actorCustomerId = null represents a system/automatic transition (e.g. auto-approval when no rule requires a step), the same convention used by Marpod_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_B2bAdvancedApproval for 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.