zarbinco / laravel-sep-pcpos
An unofficial Laravel SDK for integrating with SEP Central PC-POS services.
Requires
- php: ^8.2
- illuminate/cache: ^12.0|^13.0
- illuminate/contracts: ^12.0|^13.0
- illuminate/http: ^12.0|^13.0
- illuminate/log: ^12.0|^13.0
- illuminate/support: ^12.0|^13.0
Requires (Dev)
- larastan/larastan: ^3.10
- laravel/pint: ^1.24
- orchestra/testbench: ^10.0|^11.0
- phpunit/phpunit: ^11.5|^12.5|^13.0
README
Laravel SEP Central PC-POS is an unofficial Laravel SDK intended to help Laravel applications integrate with SEP Central PC-POS services.
The package provides typed configuration and protocol data, OAuth access-token management, unique transaction identifier retrieval, receipt customization, single or shared purchases, and transaction inquiry and reconciliation.
Disclaimer
This package is unofficial and is not affiliated with or endorsed by Saman Kish or SEP. You must obtain valid service access and credentials directly from the payment provider.
Compatibility
- PHP 8.2+
- Laravel 12 or 13
- Laravel 13 requires PHP 8.3+
Installation
Install the package with Composer:
composer require zarbinco/laravel-sep-pcpos
Laravel discovers the package service provider automatically.
Publishing configuration
Publish the configuration file with:
php artisan vendor:publish --tag=sep-pcpos-config
The published config/sep-pcpos.php file contains settings for service URLs, credentials, terminal identity, timeouts, TLS, token caching, and logging. Service URLs and sensitive values have no defaults and must be supplied for your own account and environment.
The identity URL is the complete OAuth token endpoint. The API URL is the base URL for Central PC-POS API communication. The scope must be supplied exactly as issued by the provider. Password-grant body credentials and optional HTTP Basic Authentication credentials are separate settings:
SEP_PCPOS_ENABLED=true SEP_PCPOS_IDENTITY_URL= SEP_PCPOS_API_URL= SEP_PCPOS_SCOPE= SEP_PCPOS_USERNAME= SEP_PCPOS_PASSWORD= SEP_PCPOS_BASIC_AUTH_USERNAME= SEP_PCPOS_BASIC_AUTH_PASSWORD= SEP_PCPOS_PURCHASE_ID_SHARE_WIRE_KEY= SEP_PCPOS_EVENTS_ENABLED=false SEP_PCPOS_LOGGING_ENABLED=false SEP_PCPOS_LOG_CHANNEL=
Access tokens are cached internally. When the identity service supplies a refresh token, the package uses it for renewal and safely falls back to the password grant only when the refresh grant is explicitly rejected.
Main API communication uses the configured SEP_PCPOS_API_URL, applies authentication internally, and normalizes the common SEP service response envelope. TLS verification remains enabled by default and can use a custom CA bundle. API requests are never retried automatically because repeating a financial request may be unsafe; recovery decisions belong to the operation using the transport.
Payment amounts passed to the SDK must already be expressed in the monetary unit required by the configured SEP service. The package does not silently convert toman and rial values.
Resolving the client
Dependency injection is the recommended Laravel style. The PcPosClient contract resolves the package's configured client:
use Zarbinco\LaravelSepPcpos\Contracts\PcPosClient; final class PosService { public function __construct( private readonly PcPosClient $pcPos, ) {} }
The facade is a concise alternative and resolves the same PcPosClient service:
use Zarbinco\LaravelSepPcpos\Facades\SepPcPos; $identifier = SepPcPos::receiveIdentifier();
Ordinary purchase
Resolve the operation client, request an identifier, persist that identifier in your application, and then submit an ordinary single-account purchase with an explicit terminal:
use Zarbinco\LaravelSepPcpos\Contracts\PcPosClient; use Zarbinco\LaravelSepPcpos\Data\PurchaseRequest; use Zarbinco\LaravelSepPcpos\ValueObjects\Amount; use Zarbinco\LaravelSepPcpos\ValueObjects\TerminalId; $pcPos = app(PcPosClient::class); $identifier = $pcPos->receiveIdentifier(); // Persist the identifier in your application before starting payment. $result = $pcPos->purchase( new PurchaseRequest( terminalId: TerminalId::fromString('YOUR_TERMINAL_ID'), amount: Amount::fromString('125000'), identifier: $identifier, ), ); $rrn = $result->rrn()->value();
Custom receipt
Every purchase mode accepts an optional receipt. Empty item labels and values are preserved when they are meaningful to the terminal layout:
use Zarbinco\LaravelSepPcpos\Data\Receipt\PrintItem; use Zarbinco\LaravelSepPcpos\Data\Receipt\Receipt; use Zarbinco\LaravelSepPcpos\Enums\Alignment; use Zarbinco\LaravelSepPcpos\Enums\ReceiptType; $receipt = Receipt::make('Thank you', [ new PrintItem('', '125000', Alignment::Right, ReceiptType::Both), ]); $request = new PurchaseRequest( terminalId: TerminalId::fromString('TEST_TERMINAL'), amount: Amount::fromString('125000'), identifier: $identifier, receipt: $receipt, );
Positional shared purchase
Supply amounts in the exact account order configured for the terminal. Zero placeholders are supported, and the SDK requires their exact sum to equal the total.
use Zarbinco\LaravelSepPcpos\Data\SharePurchaseRequest; $result = $pcPos->sharePurchase(new SharePurchaseRequest( terminalId: TerminalId::fromString('TEST_TERMINAL'), totalAmount: Amount::fromString('3000'), identifier: $identifier, orderedAmounts: [ Amount::fromString('1000'), Amount::fromString('0'), Amount::fromString('2000'), ], ));
The amount list order must match the account order configured for the terminal in SEP's payment switch. The SDK cannot verify that remote configuration.
IBAN/account-identifier shared purchase
Provide one to ten already-calculated shares. The SDK validates exact totals and serializes Amount_Iban, but it does not calculate business percentages. Provider-side validation remains authoritative for account identifiers.
use Zarbinco\LaravelSepPcpos\Data\IbanShare; use Zarbinco\LaravelSepPcpos\Data\IbanSharePurchaseRequest; use Zarbinco\LaravelSepPcpos\ValueObjects\Iban; $result = $pcPos->ibanSharePurchase(new IbanSharePurchaseRequest( terminalId: TerminalId::fromString('TEST_TERMINAL'), totalAmount: Amount::fromString('3000'), identifier: $identifier, shares: [ new IbanShare(Amount::fromString('1000'), Iban::fromString('TESTACCOUNT1')), new IbanShare(Amount::fromString('2000'), Iban::fromString('TESTACCOUNT2')), ], ));
Bill payment
Bill payments require the provider-issued Bill ID and Pay ID. The SDK preserves them as strings and does not apply a guessed local checksum:
use Zarbinco\LaravelSepPcpos\Data\BillPaymentRequest; use Zarbinco\LaravelSepPcpos\ValueObjects\BillId; use Zarbinco\LaravelSepPcpos\ValueObjects\PayId; $result = $pcPos->billPayment(new BillPaymentRequest( terminalId: TerminalId::fromString('TEST_TERMINAL'), identifier: $identifier, billId: BillId::fromString('TEST-BILL-001'), payId: PayId::fromString('TEST-PAY-001'), ));
Purchase-ID payment
A single Purchase-ID payment fixes the protocol transaction type and account type internally:
use Zarbinco\LaravelSepPcpos\Data\PurchaseIdPaymentRequest; use Zarbinco\LaravelSepPcpos\ValueObjects\PurchaseId; $result = $pcPos->purchaseIdPayment(new PurchaseIdPaymentRequest( terminalId: TerminalId::fromString('TEST_TERMINAL'), amount: Amount::fromString('125000'), identifier: $identifier, purchaseId: PurchaseId::fromString('TEST-PURCHASE-001'), ));
Government/shared Purchase-ID payment
Government/shared Purchase-ID payments support one to ten ordered shares and validate that their exact sum equals TotalAmount. The first share requires a Purchase ID. A missing Purchase ID on a later share is serialized as 0, as specified by SEP.
use Zarbinco\LaravelSepPcpos\Data\PurchaseIdShare; use Zarbinco\LaravelSepPcpos\Data\PurchaseIdSharePaymentRequest; use Zarbinco\LaravelSepPcpos\ValueObjects\Amount; use Zarbinco\LaravelSepPcpos\ValueObjects\Iban; use Zarbinco\LaravelSepPcpos\ValueObjects\PurchaseId; $result = $pcPos->purchaseIdSharePayment(new PurchaseIdSharePaymentRequest( terminalId: TerminalId::fromString('TEST_TERMINAL'), totalAmount: Amount::fromString('3000'), identifier: $identifier, shares: [ new PurchaseIdShare( Amount::fromString('1000'), Iban::fromString('TESTACCOUNT1'), PurchaseId::fromString('TEST-PURCHASE-001'), ), new PurchaseIdShare( Amount::fromString('2000'), Iban::fromString('TESTACCOUNT2'), ), ], ));
SEP materials in circulation use more than one spelling for the government/shared Purchase-ID compound field. Configure protocol.purchase_id_share_wire_key in config/sep-pcpos.php, or SEP_PCPOS_PURCHASE_ID_SHARE_WIRE_KEY, to the field name required by your SEP deployment. The accepted values are:
PurchaseId_Amont_Ibanfrom the StartPayment parameter tablePurchaseId_Amount_Ibanfrom the government/shared Purchase-ID instruction textPurchaseID_Amount_Ibanfrom the reviewed MerchantSample/reference payload
The SDK accepts no arbitrary field names and sends exactly one configured spelling. This setting is required only for government/shared Purchase-ID payments.
Operation observability
Lifecycle events and safe operation logging are opt-in and disabled by default. Enable them through SEP_PCPOS_EVENTS_ENABLED and SEP_PCPOS_LOGGING_ENABLED; SEP_PCPOS_LOG_CHANNEL optionally selects a Laravel logging channel.
The package dispatches PcPosOperationStarted, PcPosOperationSucceeded, and PcPosOperationFailed. Events contain only the logical operation and, for failures, a broad failure category. They intentionally exclude requests, results, exceptions, identifiers, amounts, account details, and other transaction data. Applications needing business-specific events should dispatch their own after receiving the typed SDK result.
Event listeners are operational observers, not authorization or payment-validation hooks. Listener and logger failures are isolated and never prevent an operation, replace its result or exception, or cause a retry. A listener may inspect $event->operation and a failure listener may also inspect $event->category.
When logging is enabled, fixed records contain only operation and, for failures, failure_category. The package does not automatically log financial request or response payloads and does not provide transaction audit logging.
Testing
Application tests can replace the public client with a strict, network-free fake. Configure the fake before resolving the application service under test:
use Zarbinco\LaravelSepPcpos\Enums\PcPosOperation; use Zarbinco\LaravelSepPcpos\Facades\SepPcPos; use Zarbinco\LaravelSepPcpos\Testing\PcPosFixtures; $fake = SepPcPos::fake(); $fake->queuePurchase( PcPosFixtures::purchaseResult(), ); // Run application code through the facade or an injected PcPosClient. $this->assertSame(1, $fake->callCount(PcPosOperation::Purchase));
Responses are consumed in FIFO order, requests are recorded in memory, and an unconfigured operation throws a testing configuration exception instead of fabricating success. The facade and a subsequently container-resolved PcPosClient use the same fake. Fake calls bypass transport, authentication, package events, and package logging.
There is no configuration or environment switch for fake payments. Tests must install the fake explicitly with SepPcPos::fake() before resolving application services that receive PcPosClient.
Network uncertainty can be tested without contacting SEP:
$fake->queuePurchase( PcPosFixtures::transportException(), );
The configured TransportException is thrown unchanged. It still represents an uncertain remote outcome in application logic and must not be treated as permission to retry a payment.
An uncertain public reconciliation result can be configured with PcPosFixtures::uncertainInquiryResult(). The fake returns that configured result directly; it does not reproduce the real client's internal code-97 inquiry algorithm. All fixture defaults are synthetic and may be replaced with application-specific synthetic values.
Payment requests are never retried automatically. If a connection-level
TransportExceptionoccurs after a payment request is dispatched, treat the result as uncertain. Do not submit another payment or generate a replacement identifier until the transaction has been reconciled.
Transaction inquiry and reconciliation
Use the same persisted identifier to inspect a transaction after an uncertain payment outcome. inquiry() performs exactly one non-destructive lookup:
use Zarbinco\LaravelSepPcpos\Data\InquiryRequest; $inquiry = new InquiryRequest( terminalId: TerminalId::fromString('TEST_TERMINAL'), identifier: $identifier, ); $result = $pcPos->inquiry($inquiry);
reconcile() performs the same non-destructive lookup and makes exactly one additional inquiry only when SEP returns switch response code 97:
$result = $pcPos->reconcile($inquiry); if ($result->uncertain()) { // The second response was still code 97; apply your application's policy. }
Normal inquiry explicitly sends CancelPendingRequest=false. SEP documentation states that omitting this field may expire a pending transaction, so the SDK never relies on the provider default.
Pending cancellation is a separate, explicit operation that requires the original identifier:
use Zarbinco\LaravelSepPcpos\Data\PendingCancellationRequest; $result = $pcPos->cancelPending(new PendingCancellationRequest( terminalId: TerminalId::fromString('TEST_TERMINAL'), identifier: $identifier, ));
This operation sends CancelPendingRequest=true. Its result describes the transaction state and does not by itself guarantee cancellation. Pending cancellation is never automatically retried after a connection-level failure.
For safe uncertain-payment handling, receive and persist the identifier before starting payment. If StartPayment raises TransportException, do not submit another payment; inquire or reconcile using that same identifier. The host application remains responsible for persistence and for deciding when to reconcile.
Exception guide
AuthenticationExceptioncovers identity-service and token-acquisition failures; itsAuthenticationConfigurationExceptionsubtype identifies definite local authentication setup failures.TransportConfigurationExceptionmeans the main API request was not sent because local request preparation or configuration failed.TransportExceptionmeans main API communication was attempted and the remote outcome may be uncertain. Never treat it as proof that a payment failed or as permission to retry.RemoteValidationExceptionis a structured HTTP 400 validation response and is also anApiHttpException; other known non-success HTTP statuses useApiHttpException.RemoteServiceExceptionrepresents a type-valid service envelope that reports an operation-level service error.MalformedResponseExceptionidentifies an invalid common response envelope, whileUnexpectedResponseDataExceptionidentifies invalid operation-specific data inside an otherwise valid envelope.
All package exceptions above share the PcPosException root. Application code should still handle TransportException separately because of its uncertain-outcome semantics.
Host application responsibilities
This SDK handles the protocol boundary, not the application's payment workflow. The consuming application remains responsible for persisting the identifier before StartPayment, storing its transaction and business records, preventing duplicate user submissions, deciding when to inquire or reconcile, mapping results to orders or invoices, enforcing authorization and permissions, maintaining business audit trails, and implementing UI, SMS, and allocation-percentage logic.
Security
Store credentials and other secrets in environment variables and never commit them to version control. Keep TLS verification enabled. If your environment requires a custom certificate authority, configure the CA bundle path instead of disabling certificate verification. Avoid printing, logging, or otherwise exposing resolved token values.
License
Laravel SEP Central PC-POS is open-source software licensed under the MIT License.