myopensoft/ipayment
Composer 安装命令:
composer require myopensoft/ipayment
包简介
Laravel integration with JANM iPayment (Sistem Terimaan Elektronik Kerajaan Persekutuan)
README 文档
README
Laravel integration with JANM's iPayment (Sistem Terimaan Elektronik Kerajaan Persekutuan).
The package owns the conversation with iPayment — envelopes, retries, SFTP, GPG, audit log, idempotency. Your application owns the domain — bills, payments, reconciliation. They meet at events and DTOs.
For the full picture see:
docs/architecture.md— cross-cutting: boundaries, audit log, configuration, testing, operationsdocs/protocols/BILEXT001.md— outgoing bill submissiondocs/protocols/RECEXT201.md— incoming receipt callbackdocs/protocols/RECEXT302.md— batch receipt reportdocs/protocols/ERROR9999.md— error notifications
Supported transactions
| Code | Direction | Channel | Purpose |
|---|---|---|---|
| BILEXT001 | out | HTTPS realtime | Submit / cancel / increase / decrease / update a bill |
| RECEXT201 | in | HTTPS realtime | iPayment posts a receipt to your callback |
| RECEXT302 | in | SFTP batch | End-of-day receipts (GPG-encrypted) |
| ERROR9999 | in | SFTP batch | Async error notifications (GPG-encrypted) |
Architecture at a glance
flowchart LR
H[Your Laravel app] -->|sendBill| F[Ipayment facade]
F --> AC[ApiClient]
AC -->|POST BILEXT001| API[(JANM API)]
AC --> AL[(ipayment_messages<br/>audit log)]
API -->|POST RECEXT201| CB[ReceiptCallbackController]
CB --> AL
CB -. ReceiptReceived .-> H
SFTP[(JANM SFTP)] -->|.gpg files| BP[BatchPuller]
BP --> AL
BP -. BatchReceiptParsed / IpaymentErrorReceived .-> H
The host application listens to events and writes to its own tables; it never has to know about envelopes, GPG, or pipe delimiters.
Installation
composer require myopensoft/ipayment
php artisan vendor:publish --tag=ipayment-config
php artisan migrate
Optional models (only if your app does not already model bills / payments):
php artisan vendor:publish --tag=ipayment-models
php artisan migrate
Configuration
Required .env keys:
IPAYMENT_AGENCY_CODE=0100010001
IPAYMENT_SERVICE_CODE=S00001
IPAYMENT_ENV=sit
IPAYMENT_API_URL=https://ipayment.test/api
IPAYMENT_API_USER=...
IPAYMENT_API_PASS=...
IPAYMENT_SFTP_HOST=esb-ipayment.anm.gov.my
IPAYMENT_SFTP_PORT=2222
IPAYMENT_SFTP_USER=...
IPAYMENT_SFTP_KEY=/path/to/private_key
IPAYMENT_GPG_HOME=/path/to/gnupg-home
IPAYMENT_GPG_RECIPIENT=ipayment@anm.gov.my
IPAYMENT_GPG_SIGNING_KEY=your-key-id
IPAYMENT_GPG_PASSPHRASE=...
Verify everything is wired:
php artisan ipayment:check
Sending a bill (BILEXT001)
use Carbon\Carbon;
use MyOpensoft\Ipayment\Data\AddressDto;
use MyOpensoft\Ipayment\Data\BillDto;
use MyOpensoft\Ipayment\Data\ChargeLineDto;
use MyOpensoft\Ipayment\Data\CustomerDto;
use MyOpensoft\Ipayment\Facades\Ipayment;
$bill = BillDto::newBill(
reference: 'INV-2026-0001',
amount: 150.00,
billDate: Carbon::now(),
customer: CustomerDto::individual(
ic: '900101011234',
name: 'Ahmad Bin Ali',
address: new AddressDto('No 1 Jln A', null, null, '50100', '14', 'MY'),
phone: '0123456789',
email: 'ahmad@example.com',
),
chargeLines: [
ChargeLineDto::make(
classificationCode: 'INV_H0161199',
ptjGroup: '28020101',
voteFund: 'B1024',
programActivity: 'B10010104',
accountCode: 'H0171101',
amount: 150.00,
),
],
);
$response = Ipayment::sendBill($bill);
if ($response->isSuccess()) {
// Persist $response->messageRef alongside your bill.
}
The bill carries a jenis_maklumat_terimaan ("info type") code which
describes what kind of charge it represents. BillDto::newBill()
defaults to "01" — a fixed-amount bill with one or more charge lines.
The other three info types are encoded as constants on BillDto:
| Code | English label | Constant | Charge lines |
|---|---|---|---|
01 | Bill (with amount) | INFO_TYPE_BILL | Multiple |
02 | Bill without amount | INFO_TYPE_BILL_NO_AMOUNT | Single |
03 | Payment without bill or amount | INFO_TYPE_PAYMENT_NO_BILL_AMOUNT | Single |
04 | Payment without rate (open amount) | INFO_TYPE_PAYMENT_NO_RATE | Multiple |
Mutations are first-class — each returns an ApiResponseDto:
Ipayment::cancelBill('INV-2026-0001', 'Paid at counter', now());
Ipayment::addBillAmount('INV-2026-0001', 25.00, now(), 'Surcharge');
Ipayment::reduceBillAmount('INV-2026-0001', 10.00, now(), 'Adjustment');
Ipayment::updateBill($bill);
Listen for outcomes:
use MyOpensoft\Ipayment\Events\BillSent;
use MyOpensoft\Ipayment\Events\BillSendFailed;
Event::listen(BillSent::class, fn ($e) => /* mark sent */ null);
Event::listen(BillSendFailed::class, fn ($e) => /* alert / retry queue */ null);
Receiving receipts (RECEXT201)
The package registers POST /ipayment/receipt (route prefix configurable).
Listen for the event and write to your own tables:
use MyOpensoft\Ipayment\Events\ReceiptReceived;
Event::listen(ReceiptReceived::class, function (ReceiptReceived $e) {
Payment::create([
'gateway' => 'ipayment',
'reference' => $e->receipt->billReference,
'receipt_number' => $e->receipt->receiptNumber,
'amount' => $e->receipt->amount,
'payment_mode' => $e->receipt->paymentMode,
// ...
]);
});
Lock the callback down via middleware:
// config/ipayment.php
'callback' => [
'prefix' => 'ipayment',
'middleware' => ['api', 'auth.basic'],
],
Idempotency
JANM may retry callbacks. To dedupe, point the package at a resolver:
'receipt_resolver' => \MyOpensoft\Ipayment\Support\AuditLogReceiptResolver::class,
The bundled resolver checks the audit log for a settled (response_code='00')
prior row with the same message_ref or receipt_number. Write your
own implementing MyOpensoft\Ipayment\Contracts\ReceiptResolver to
consult your domain tables instead.
Batch SFTP (RECEXT302, ERROR9999)
Schedule the pull (typical cadence: every 15 minutes):
// app/Console/Kernel.php
$schedule->command('ipayment:pull-batch')->everyFifteenMinutes();
Then listen for the per-row events:
use MyOpensoft\Ipayment\Events\BatchReceiptParsed;
use MyOpensoft\Ipayment\Events\IpaymentErrorReceived;
use MyOpensoft\Ipayment\Events\BatchFileProcessed;
Event::listen(BatchReceiptParsed::class, fn ($e) => /* reconcile */);
Event::listen(IpaymentErrorReceived::class, fn ($e) => /* alert */);
Event::listen(BatchFileProcessed::class, fn ($e) => /* metrics */);
Decrypted files are archived under storage/app/ipayment/archive;
files that fail to decrypt or parse go to quarantine/.
Audit log
Every message the package sends or receives is written to
ipayment_messages before any event fires. Schema:
| Column | Purpose |
|---|---|
direction | in / out |
process_code | BILEXT001, RECEXT201, RECEXT302, ERROR9999 |
message_ref | 40-char nombor_rujukan_message (indexed) |
receipt_number | nombor_resit for RECEXT201 / RECEXT302 (indexed) |
response_code | kod_respond — 00 / 01 / 02 |
request_payload | the JSON envelope sent or received |
response_payload | the JSON sent back, or exception details |
source_file | batch filename for SFTP rows |
http_status | for outgoing rows |
This table is the single source of truth for "did we see this message" and powers the default idempotency resolver.
Host responsibilities (out of scope)
- Master data — services, charge lines, PTJ groups, vot/dana mapping
- Bill / payment / receipt domain model (or use the optional models)
- Reconciliation policy
- IP whitelisting, SFTP key exchange, GPG key exchange (operational)
- Scheduling
ipayment:pull-batch
Language
The iPayment protocol's JSON keys are in Malay (mesej, kod_proses,
kod_respond, maklumat_resit, …) and the package keeps them that way
on the wire — that's not negotiable. Everything else — PHP
identifiers, audit log columns, status enum values — is English.
License
MIT
统计信息
- 总下载量: 0
- 月度下载量: 0
- 日度下载量: 0
- 收藏数: 0
- 点击次数: 4
- 依赖项目数: 0
- 推荐数: 0
其他信息
- 授权协议: MIT
- 更新时间: 2026-05-29