Building a Bridge
Hyvä POS is in closed beta
Hyvä POS is currently in a closed beta (pilot phase) with a small group of merchants. It is not yet generally available: the App Store release follows the pilot, and features and configuration may still change - possibly in backwards-incompatible ways - before the general release. Want to take part? Sign up at hyva.io/pos.
Bridges are how third-party modules plug into Hyvä POS. The app calls one universal API for store credit, gift cards, reward points, RMA and the other provider-backed features; on the Magento side, a bridge translates that call to your module's own services. Adobe Commerce, Aheadworks, Amasty and Mirasvit bridges ship ready-made; this section is for building your own.
This page walks through building a complete bridge module, from empty directory to passing conformance tests. Each provider type then has its own page with the interface, the contract rules, and a worked implementation:
| Provider type | Page | Pool |
|---|---|---|
| Store credit | Store Credit | Hyva\Pos\Model\CreditProviderPool |
| Gift cards | Gift Cards | Hyva\Pos\Model\CreditProviderPool |
| Reward points | Reward Points | Hyva\Pos\Model\CreditProviderPool |
| Returns / RMA | RMA Providers | Hyva\Pos\Model\RmaProviderPool |
| Shipping labels | Shipping Label Providers | Hyva\Pos\Model\ShippingLabelProviderPool |
| Credit limits | Credit Limit Resolvers | Hyva\Pos\Model\Customer\CreditLimitResolverPool |
| Customer validation | Customer Validators | Hyva\Pos\Model\Customer\CustomerValidatorPool |
| Customer pricing | Customer Pricing Resolvers | Hyva\Pos\Model\Customer\CustomerPriceResolverPool |
The Pool Architecture
Every provider type has a pool. The pool holds every registered provider of its kind and resolves the active one; the core module only ever invokes the active provider. That makes co-installation safe structurally: your bridge never checks instanceof against a sibling bridge, and two wallet vendors can be installed side by side while only the configured one handles POS traffic.
The merchant picks the active provider per feature in Stores → Configuration → Hyvä POS → Advanced → Credit & Return Providers. When nothing is configured, the pool auto-detects the first installed provider for that feature.
Two pools behave differently, by design:
- The customer validator pool does not pick one winner. Every registered validator runs and their results merge into one list, so the cashier sees every problem in a single save round-trip.
- The shipping label pool resolves by carrier: each provider declares which shipments it can handle via
canHandle(), and the first match wins.
The Contract Rules
These rules are what keep money safe across every bridge. They are non-negotiable:
- The core owns the transaction. Credit debits and refund-to-credit run inside a database transaction the core module opens. Your provider performs the wallet mutation and throws on failure; it never opens its own transaction and never commits.
- The core owns idempotency. Ledger tables in the core record what has been applied, so retries (an offline queue draining twice, a memo saved again) cannot double-debit or double-credit. Your provider must not build its own bookkeeping on top.
- Failure semantics differ by direction. A refund-to-credit that throws rolls the credit memo back: no memo without money moved. A checkout debit that throws does not block the order: the order saves, and the debit retries on the next save. Design your provider's error behavior accordingly.
- Speak the canonical vocabulary. RMA providers map their vendor's statuses onto the canonical status set; credit providers report balances in the shapes the contract defines. The app never sees vendor-specific values.
applyCreditis validate-only, for every wallet type. Checkout apply validates; the wallet write happens once, at order save, through the core dispatcher. A provider that mutates the wallet insideapplyCreditdouble-debits.
Anatomy of a Bridge Module
The shipped example bridges are the reference skeleton. A minimal bridge is seven files:
magento2-acme-pos-bridge/
├── composer.json
├── src/
│ ├── registration.php
│ ├── etc/
│ │ ├── module.xml
│ │ └── di.xml
│ └── Model/
│ └── StoreCreditProvider.php
└── tests/
├── bootstrap.php
└── unit/
└── StoreCreditProviderConformanceTest.php
Build One Step by Step
1. Scaffold the Module
Declare the package as a magento2-module and autoload registration.php:
{
"name": "acme/magento2-acme-pos-bridge",
"description": "Connects Acme Wallet to Hyva POS",
"type": "magento2-module",
"require": {
"php": ">=8.1",
"hyva-themes/magento2-hyva-pos": "*"
},
"autoload": {
"files": ["src/registration.php"],
"psr-4": { "Acme\\PosBridge\\": "src/" }
}
}
Do not add a hard composer requirement on your vendor module if merchants might install your bridge without it - the registration guard in step 2 handles the missing-vendor case gracefully, and a suggest entry documents the relationship without forcing it.
Declare the load order so the pool exists before your di.xml plugs into it:
<config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:noNamespaceSchemaLocation="urn:magento:framework:Module/etc/module.xsd">
<module name="Acme_PosBridge">
<sequence>
<module name="Hyva_Pos"/>
<module name="Acme_Wallet"/>
</sequence>
</module>
</config>
2. Guard the Registration
Your provider constructor-injects the vendor's typed interfaces. During setup:di:compile, PHP reflection autoloads those parameter types - and crashes the compile if the vendor module is absent. Guard registration.php on a vendor interface so the bridge simply does not register when the vendor is missing:
<?php
declare(strict_types=1);
use Magento\Framework\Component\ComponentRegistrar;
if (!interface_exists(\Acme\Wallet\Api\WalletRepositoryInterface::class)) {
return;
}
ComponentRegistrar::register(ComponentRegistrar::MODULE, 'Acme_PosBridge', __DIR__);
Guard on a real vendor class or interface, never on a code-generated *Factory or *Extension class. Generated classes are not reliably autoloadable during CLI module discovery, so a factory guard makes your module silently deregister itself on setup:upgrade even when the vendor is installed.
3. Implement the Provider
This is where the provider pages take over: each one documents its interface with full signatures, the contract clauses, and a worked implementation based on a shipped bridge. Pick yours from the table at the top of this page.
4. Register It in the Pool
Add your provider to the pool's array argument:
<config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:noNamespaceSchemaLocation="urn:magento:framework:ObjectManager/etc/config.xsd">
<type name="Hyva\Pos\Model\CreditProviderPool">
<arguments>
<argument name="providers" xsi:type="array">
<item name="acme_store_credit" xsi:type="string">Acme\PosBridge\Model\StoreCreditProvider</item>
</argument>
</arguments>
</type>
</config>
Whether the item is xsi:type="string" or xsi:type="object" depends on the pool, and getting it wrong on the small pools fails silently:
| Pool | Argument | Item type |
|---|---|---|
CreditProviderPool |
providers |
string - resolved lazily in a try/catch, so an absent vendor is skipped instead of crashing DI compilation |
RmaProviderPool |
providers |
string - same lazy resolution |
ShippingLabelProviderPool |
providers |
object - no string resolution; a string entry is ignored |
CreditLimitResolverPool |
resolvers |
object - a string entry is silently skipped and your resolver never runs |
CustomerValidatorPool |
validators |
object |
CustomerPriceResolverPool |
resolvers |
object |
For the object pools, the missing-vendor case is handled entirely by the registration guard from step 2: when the vendor is absent your module never registers, so the pool never sees the entry. Each provider page shows the exact registration for its pool.
5. Whitelist Any New Schema
If your bridge declares its own db_schema.xml tables or columns, add them to db_schema_whitelist.json. Declarative schema silently skips element modifications that are not whitelisted - the symptom is a table that never appears, with no error anywhere.
6. Install and Verify
composer require acme/magento2-acme-pos-bridge
bin/magento setup:upgrade
bin/magento setup:di:compile
bin/magento module:status Acme_PosBridge
Then verify the wiring end to end: set your provider as active under Stores → Configuration → Hyvä POS → Advanced → Credit & Return Providers, and exercise the feature from the POS app. Each provider page lists what to check at the register.
Two checks worth doing before you call it done:
- Uninstall (or rename) the vendor module and run
setup:di:compileagain. It must succeed, with your bridge absent frommodule:status. That proves the registration guard. - Reinstall the vendor and run
setup:upgrade. Your bridge must re-register. That proves the guard is on an autoloadable class.
Prove It: the Conformance Kit
The core ships an executable conformance kit under Hyva\Pos\TestFramework\Conformance\ - abstract PHPUnit test cases that encode the contract as assertions, so you do not have to re-derive what "correct" means. Subclass the abstract for your provider type and supply only the vendor wiring:
<?php
declare(strict_types=1);
namespace Acme\PosBridge\Test\Unit;
use Acme\PosBridge\Model\StoreCreditProvider;
use Hyva\Pos\Api\CreditProviderInterface;
use Hyva\Pos\TestFramework\Conformance\AbstractCreditProviderConformanceTest;
class StoreCreditProviderConformanceTest extends AbstractCreditProviderConformanceTest
{
protected function createProvider(): CreditProviderInterface
{
return new StoreCreditProvider(/* your vendor mocks */);
}
}
Scenarios are arranged through small arrange*() hooks you wire to your vendor mocks. Skip semantics differ by kit: the credit kit skips scenarios for optional interfaces your provider does not implement, while the RMA, validator and price kits skip a scenario when its arrange hook returns null (for a case your backend genuinely cannot reach). Assertions are message-pattern based, so your cashier-facing wording stays yours (and localizable). All four shipped bridges run the kit in CI; yours should too. The abstract for each provider type is documented on its provider page.
Reference Implementations
| Package | What it demonstrates |
|---|---|
magento2-hyva-pos-adobe-commerce |
Store credit, gift cards and reward points on Adobe Commerce's native modules, including the native-pipeline refund pattern |
magento2-hyva-pos-amasty |
Amasty Store Credit, Gift Card and Reward Points, including consumption-return sugar |
magento2-hyva-pos-aheadworks |
Aheadworks Store Credit, Gift Card, Reward Points and RMA |
magento2-hyva-pos-mirasvit |
Mirasvit Store Credit and Reward Points |
| Example bridges | Two deliberately small proof-of-concept bridges (a customer validator and a customer price resolver) built to be read top to bottom |
Where to Start
Read the provider page for your feature, then clone one of the example bridges and adapt it. The examples are small enough to read in one sitting and carry the same file layout, registration guard commentary, and conformance test adoption you need in a real bridge.