orangecat / module-company-credit
Company credit payment method and balance management for Magento 2 B2B — credit limits, ledger tracking, and automatic refund handling
Package info
github.com/olivertar/m2_companycredit
Type:magento2-module
pkg:composer/orangecat/module-company-credit
Requires
- php: >=8.2
- magento/framework: *
- orangecat/core: *
- orangecat/module-company: *
README
B2B company credit payment method and balance management for Magento 2. Assigns configurable credit limits per company, tracks usage via an immutable ledger, and exposes balance information to both store admins and company admins on the frontend.
Module: Orangecat_CompanyCredit
Version: 1.0.0
License: OSL-3.0
Author: Oliverio Gombert olivertar@gmail.com
Table of Contents
- Overview
- Theme Compatibility
- Requirements
- Installation
- What Gets Installed
- Credit Balance Semantics
- Configuration Reference
- Store Admin Guide
- Company Admin Guide (Frontend)
- Developer Guide
- REST API
- Frontend Routes Reference
- DevOps & Integrator Notes
Overview
Orangecat_CompanyCredit extends Orangecat_Company with a credit-based payment system. Companies receive a configurable credit line; purchases consume credit, cancellations and credit memos restore it, and every transaction is logged in an auditable ledger.
The module handles:
- Offline checkout payment method (
companycredit) restricted to company customers - Per-company credit limit, currency, and exceed-limit toggle
- Immutable transaction ledger with automatic debit/credit on order events
- Admin UI for credit configuration and manual adjustments, embedded in the company edit form
- Frontend credit page for company admins (balance summary + transaction history)
- Full REST API for credit and ledger management
Position in the Orangecat B2B Dependency Chain
Orangecat_Core (via composer: orangecat/core)
└── Orangecat_Company
└── Orangecat_CompanyCredit ← this module
Theme Compatibility
| Theme | Status | Notes |
|---|---|---|
| Luma | Supported | Standard .phtml template. Less styles via _module.less. |
| Hyvä | Supported | Dedicated index_hyva.phtml template with Tailwind CSS. Loaded via hyva_* layout handles. |
| Breeze Evolution | Partial | No dedicated Breeze templates. Falls back to Luma index.phtml. Checkout payment JS uses standard RequireJS (Breeze-compatible). |
For Breeze-specific overrides: create view/frontend/templates/credit/index_breeze.phtml and view/frontend/layout/breeze_company_credit_index.xml following Breeze conventions.
Requirements
| Dependency | Version |
|---|---|
| PHP | 8.2+ |
| Magento | 2.4.x |
Orangecat_Core |
composer: orangecat/core |
Orangecat_Company |
must be enabled |
Magento_Sales |
core |
Magento_Payment |
core |
Installation
Run inside the PHP container (reward shell):
bin/magento module:enable Orangecat_CompanyCredit bin/magento setup:upgrade bin/magento setup:di:compile bin/magento setup:static-content:deploy -f bin/magento cache:flush
What Gets Installed
Database Tables
mycompany_credit
One record per company (1:1). Stores the credit configuration and running balance.
| Column | Type | Notes |
|---|---|---|
entity_id |
int, PK | Auto-increment |
company_id |
int, UNIQUE FK | → mycompany.entity_id (CASCADE DELETE) |
currency_code |
varchar(3) | ISO 4217, nullable |
credit_limit |
decimal(20,4) | Default 0.0000 |
balance |
decimal(20,4) | Default 0.0000 — outstanding debt |
allow_exceed_limit |
smallint | 0 = no, 1 = yes |
mycompany_credit_ledger
Append-only audit log. One row per credit event.
| Column | Type | Notes |
|---|---|---|
entity_id |
int, PK | Auto-increment |
company_id |
int, FK | → mycompany.entity_id (CASCADE DELETE) |
amount |
decimal(20,4) | Negative = purchase/debit; positive = refund/credit |
order_id |
varchar(50) | Order increment ID, nullable |
comment |
text | Nullable |
created_at |
timestamp | Auto CURRENT_TIMESTAMP |
No data patches, EAV attributes, or CMS pages are installed by this module.
Credit Balance Semantics
Understanding the sign convention is critical:
| Concept | Column | Direction | Example |
|---|---|---|---|
| Balance | mycompany_credit.balance |
Increases on purchase, decreases on refund | $1,500 outstanding debt |
| Credit Limit | mycompany_credit.credit_limit |
Static ceiling | $10,000 |
| Available Credit | Computed: limit − balance |
Decreases on purchase | $8,500 |
| Ledger: purchase | mycompany_credit_ledger.amount |
Negative number | −$1,500 |
| Ledger: refund | mycompany_credit_ledger.amount |
Positive number | +$500 |
A company starts with balance = 0 and available = credit_limit. Each purchase increases balance. Refunds and cancellations decrease balance.
Configuration Reference
Store Configuration — My Company > Company Credit
Path: Stores → Configuration → My Company → Company Credit
| Label | Config Path | Default | Description |
|---|---|---|---|
| Enable Company Credit | mycompany/company_credit/enabled |
Yes | Master switch. Disables the frontend menu link and the payment method. |
| Refund Credit on Order Cancel | mycompany/company_credit/refund_on_cancel |
Yes | Automatically restore credit when a company credit order is cancelled. |
| Refund Credit on Credit Memo | mycompany/company_credit/refund_on_creditmemo |
Yes | Automatically restore credit when a credit memo is issued. |
Payment Methods — Company Credit
Path: Stores → Configuration → Sales → Payment Methods → Company Credit
| Label | Config Path | Default | Description |
|---|---|---|---|
| Enabled | payment/companycredit/active |
Yes | Activates the payment method entry. Still hidden if mycompany/company_credit/enabled = No. |
| Title | payment/companycredit/title |
Company Credit | Label shown at checkout. |
| New Order Status | payment/companycredit/order_status |
Pending | Status assigned to new orders placed with this method. |
| Payment from Applicable Countries | payment/companycredit/allowspecific |
All Allowed Countries | Restrict to specific countries if needed. |
| Sort Order | payment/companycredit/sort_order |
— | Display order among payment methods. |
Per-Company Credit Settings (Admin Company Form)
These are stored in mycompany_credit, not core_config_data:
| Field | Description |
|---|---|
| Currency Code | Currency for this company's credit line (must match store currency at checkout). |
| Credit Limit | Maximum credit amount the company can carry as outstanding balance. |
| Allow Exceed Limit | Yes: company can place orders even if order total > available credit. |
Store Admin Guide
Accessing Credit Settings
- Go to Companies (main menu) → open any company.
- The Company Credit fieldset appears in the company edit form.
Credit Fieldset
| UI Element | Purpose |
|---|---|
| Outstanding Balance | Read-only. Current used credit (balance). |
| Available Credit | Read-only. Computed: credit_limit − balance. |
| Credit Limit | Editable. Set the ceiling for this company. |
| Currency Code | Editable. ISO 4217 code (e.g., USD, EUR). |
| Allow Exceed Limit | Editable. Yes/No toggle. |
Save the company form to persist changes. Credit data is saved via CompanyRepositoryPlugin after the main company save.
Transaction Ledger Grid
Below the credit fieldset, an embedded grid lists all ledger entries for the company:
| Column | Description |
|---|---|
| ID | entity_id |
| Amount | Positive = refund/credit; negative = purchase/debit. |
| Order | Related order increment ID (if applicable). |
| Comment | Free-text note. |
| Date | UTC timestamp. |
Manual Refund / Adjustment
- Click Refund button in the credit fieldset.
- A modal opens with three fields:
- Amount (required, must be > 0) — credit to restore.
- Order ID (optional) — reference to a specific order.
- Comment (required) — reason for adjustment.
- Submit. The system creates a positive ledger entry and decreases
balanceaccordingly.
To edit an existing manual entry (amount > 0, no order ID), click the row action in the ledger grid. The modal reopens pre-populated; saving recalculates the balance delta.
Company Admin Guide (Frontend)
Where to Find Credit Information
After logging in, go to My Account → Company Credit (link in the account navigation sidebar).
Direct URL: https://<store-domain>/company/credit/index
What You See
Credit Summary (three tiles at the top):
| Tile | Meaning |
|---|---|
| Outstanding Balance | How much your company currently owes. |
| Available Credit | How much you can still spend (limit − outstanding). |
| Credit Limit | Maximum credit your company has been granted. |
Transaction History table below the tiles:
| Column | Meaning |
|---|---|
| Date | When the transaction occurred. |
| Amount | Positive (green) = credit was restored. Negative (red) = credit was consumed. |
| Comment | Description (order reference or admin note). |
The list is sorted newest-first. An empty-state message appears if no transactions exist yet.
Paying with Company Credit at Checkout
If your company has available credit (or is allowed to exceed the limit), Company Credit appears as a payment option at checkout. Select it and place the order — no card or external payment is required. The amount is deducted from your available credit immediately upon order placement.
If the payment method is not visible, one of the following applies:
- The feature is disabled by the store admin.
- Your company has no credit configuration.
- The order currency does not match your company's credit currency.
- Your available credit is insufficient (and the store does not allow exceeding the limit).
Developer Guide
Module Structure
CompanyCredit/
├── Api/ # Service contracts and data interfaces
├── Block/ # View blocks (frontend credit page, admin fieldset, modal)
├── Controller/
│ ├── Credit/Index.php # Frontend credit page
│ └── Adminhtml/Ledger/Save.php # Admin manual refund save
├── Model/
│ ├── Credit.php # Entity: mycompany_credit
│ ├── Ledger.php # Entity: mycompany_credit_ledger
│ ├── CreditRepository.php
│ ├── LedgerRepository.php
│ ├── CreditManagement.php # Service layer
│ └── Payment/CompanyCredit.php # Payment method
├── Observer/ # Three event observers (see below)
├── Plugin/ # Two interceptors (see below)
├── Ui/Component/ # Admin grid data provider + actions column
└── view/
├── adminhtml/ # Admin templates, UI component XML, JS
└── frontend/ # Luma and Hyvä templates, layouts, Less, CSS, JS
Key Classes
Service Layer
Model/CreditManagement.php — implements Api/CreditManagementInterface.php. All business operations go through here:
| Method | Description |
|---|---|
updateCredit(companyId, currencyCode, creditLimit, allowExceedLimit) |
Upsert credit record. Only non-null parameters are updated. |
getBalance(companyId) |
Return CreditInterface with current state. |
getLedgerByCompanyId(companyId) |
Return ledger entries sorted by created_at DESC. |
refund(companyId, amount, comment) |
Create positive ledger entry, decrease balance. Validates amount > 0. |
updateLedgerEntry(entryId, amount, comment) |
Edit existing entry; recalculates balance delta. |
Payment Method
Model/Payment/CompanyCredit.php — extends Magento\Payment\Model\Method\AbstractMethod. Payment code: companycredit.
isAvailable(?CartInterface $quote) returns false if any of these fail:
mycompany/company_credit/enabledis off.- Customer not logged in or not in a company.
- No credit record for the company.
- Currency code mismatch between quote and credit record.
quote->getGrandTotal() > available creditANDallow_exceed_limit = 0.
Observers
| Class | Event | Action |
|---|---|---|
Observer/OrderPlaceObserver |
sales_model_service_quote_submit_success |
Creates negative ledger entry; increases balance. |
Observer/OrderCancelObserver |
order_cancel_after |
If refund_on_cancel enabled: finds original entry by order ID, creates positive entry, decreases balance. |
Observer/CreditMemoRefundObserver |
sales_order_creditmemo_save_after |
If refund_on_creditmemo enabled: same logic as cancel observer but for credit memo amount. |
All observers are no-ops when the order's payment method is not companycredit.
Plugins
| Class | Target | Hook | Purpose |
|---|---|---|---|
Plugin/Api/CompanyRepositoryPlugin |
Orangecat\Company\Api\CompanyRepositoryInterface |
afterSave() |
Reads company_credit POST data; upserts mycompany_credit record after company save. |
Plugin/Ui/Component/Form/Company/DataProviderPlugin |
Orangecat\Company\Ui\Component\Form\Company\DataProvider |
afterGetData() |
Injects credit_limit, currency_code, allow_exceed_limit into company form data from mycompany_credit. |
Both plugins are registered in etc/adminhtml/di.xml (admin area only).
Frontend JS
| File | Purpose |
|---|---|
view/frontend/web/js/view/payment/companycredit.js |
Registers companycredit renderer in Magento checkout JS. |
view/frontend/web/js/view/payment/method-renderer/companycredit-method.js |
Extends Magento_Checkout/js/view/payment/default. |
view/frontend/web/template/payment/companycredit.html |
Knockout template for the checkout payment step. |
Admin JS
| File | Purpose |
|---|---|
view/adminhtml/web/js/grid/columns/refund-actions.js |
Extends Magento_Ui/js/grid/columns/actions. Manages modal open/close, pre-populates the refund form for edit mode, clears fields on new entry. |
ACL Resources
| Resource ID | Title | Location |
|---|---|---|
Orangecat_CompanyCredit::credit |
Company Credit | Under Orangecat_Company::company_manage |
Adding Custom Logic
Before credit is deducted — listen to sales_model_service_quote_submit_success (same event, earlier sort order) or create a plugin on CreditManagement::updateCredit.
To extend the Credit or Ledger data objects — add a new preference or extension attribute via extension_attributes.xml targeting CreditInterface / LedgerInterface.
To add a column to the ledger grid — extend view/adminhtml/ui_component/mycompany_credit_ledger_listing.xml via a module merge.
REST API
All endpoints require the Orangecat_CompanyCredit::credit ACL resource (admin token or integration token with that permission).
| Method | Endpoint | Description |
|---|---|---|
| GET | /V1/mycompany/credit/:companyId |
Get credit balance and configuration for a company. |
| PUT | /V1/mycompany/credit/:companyId |
Update credit limit, currency, or allow-exceed flag. |
| GET | /V1/mycompany/credit/:companyId/ledger |
Get all ledger entries for a company (newest first). |
| POST | /V1/mycompany/credit/:companyId/refund |
Create a manual credit refund entry. |
| PUT | /V1/mycompany/credit/ledger/:entryId |
Edit an existing ledger entry and recalculate balance. |
| DELETE | /V1/mycompany/credit/ledger/:entityId |
Delete a ledger entry by ID. |
Examples
# Get credit state for company 5 GET /rest/V1/mycompany/credit/5 # Set credit limit to $50,000 USD, disallow exceed PUT /rest/V1/mycompany/credit/5 Content-Type: application/json { "creditLimit": 50000, "currencyCode": "USD", "allowExceedLimit": 0 } # Manual refund of $1,200 POST /rest/V1/mycompany/credit/5/refund Content-Type: application/json { "amount": 1200, "comment": "Goodwill adjustment — ticket #4412" } # Get ledger history GET /rest/V1/mycompany/credit/5/ledger
Frontend Routes Reference
| Route | Controller | Access |
|---|---|---|
GET /company/credit/index |
Controller\Credit\Index |
Logged-in company customer |
DevOps & Integrator Notes
Deployment Checklist
# After deploying or updating this module: bin/magento setup:upgrade # runs db_schema.xml migrations bin/magento setup:di:compile # regenerates interceptors and proxies bin/magento setup:static-content:deploy -f bin/magento cache:flush
Integration Token Scope
When creating a Magento integration for ERP/external system credit sync, grant:
Orangecat_CompanyCredit::creditOrangecat_Company::company_manage(parent required)
Disabling Without Uninstalling
Set mycompany/company_credit/enabled = No in store config. This hides the frontend menu item and makes the payment method unavailable at checkout without removing any data or database schema.
To also hide the payment method from the admin payment method list: set payment/companycredit/active = No.
Data Integrity
- Both tables use
ON DELETE CASCADEfrommycompany.entity_id. Deleting a company removes all its credit and ledger data automatically. - There is no soft-delete on ledger entries. Manual deletions via the REST API are permanent.
- Currency code is stored per-company. If your store currency changes, update the credit record accordingly or the payment method will be unavailable at checkout (currency mismatch check in
isAvailable).