Weave Code
Weaver
Helps Laravel developers discover, compare, and choose open-source packages. See popularity, security, maintainers, and scores at a glance to make better decisions.
Feedback
Share your thoughts, report bugs, or suggest improvements.
Subject
Message

Myinvois Laravel Package

jiannius/myinvois

A Laravel package for integrating with Malaysia's MyInvois e-Invoicing platform, offering helpers to submit, validate, and manage electronic invoices through the LHDN MyInvois API from PHP applications.

View on GitHub
Deep Wiki
Context7

Getting Started

v1.5.0 is additive: submitDocuments() still works exactly as before, so the upgrade itself is a composer update plus a config check. Everything else is opt-in and can be adopted incrementally.

  1. Upgrade and re-publish config — the new keys live under services.myinvois.*. Diff your existing config rather than overwriting it.
  2. Check your queue retry window — queue.retry_after (in config/queue.php) must exceed 120s. If it doesn't, a PollSubmission job can be retried while the first attempt is still polling, and you'll get duplicate work.
  3. Verify the schedule actually registers — the myinvois:sync schedule is only added when services.myinvois.sync_schedule is set. Run php artisan schedule:list and confirm it appears before assuming it's running.
  4. Dry-run the sweep on preprod first:
php artisan myinvois:sync --limit=50 --dry-run

First use case: take one heavyweight endpoint — typically the controller action that calls submitDocuments() — and move it to the queue. The controller dispatches; a listener on SubmissionAccepted updates your UI. That single change removes the LHDN round-trip (and its timeouts) from the web request, which is the whole point of the release.

If you're not ready for that, still take the fixes: the shared rate limiter, the null-status handling, and the failedCallback null crash are behaviour changes that affect every app.

Implementation Patterns

Dispatch from the controller, react to events. Don't poll from the request either.

public function store(InvoiceRequest $request)
{
    $parent = Invoice::create($request->validated());

    Myinvois::queueSubmission(
        documents: [$parent->toMyinvoisDocument()],
        parents: [$parent],
        context: ['team_id' => $parent->team_id],
    );

    return back()->with('status', 'Submitted for validation');
}

Then keep the UI honest with listeners rather than re-reading state:

Event::listen(SubmissionSucceeded::class, fn ($e) => $e->parent->markValid());
Event::listen(SubmissionFailed::class, fn ($e) => $e->parent->markRejected($e->reasons));
Event::listen(DocumentStatusChanged::class, fn ($e) => Cache::forget("doc:{$e->document->id}"));

SubmissionFailed carries LHDN's reasons for INVALID — surface those verbatim to users, they're the only actionable explanation you'll get.

Treat the in-flight lock as a feature, not an obstacle. The package refuses to re-submit a parent whose latest submission is SUBMITTED or VALID. Translate MyinvoisInFlightException into a 409 rather than swallowing it:

try {
    Myinvois::queueSubmission([$doc], [$parent], $context);
} catch (MyinvoisInFlightException $e) {
    abort(409, 'This invoice is already being submitted.');
}

This is what makes it safe to dispatch from a button a user can double-click.

Per-organisation credentials via the contract. Bind ResolvesMyinvoisClient in a service provider and resolve from context, not from the authenticated user — the worker has no session.

$this->app->bind(ResolvesMyinvoisClient::class, fn () => new OrgClientResolver());
// worker side
$client = Myinvois::clientFor($context['team_id']);

One client per submission is enforced; don't try to swap credentials mid-flight.

Use the sync command as your reconciliation loop, not a webhook. forParents() narrows the sweep, withoutPolling() skips documents still in flight. Default behaviour skips already-VALID parents — add --include-valid only if you're deliberately re-checking them.

Test the whole flow without touching LHDN.

Myinvois::fake()->pollResolvesTo('VALID');

Cover the failure path too — a SubmissionFailed with reasons is the branch most likely to break your UI.

Gotchas and Tips

  • retry_after > 120s is not optional. This is the single most common misconfiguration: polling jobs get retried mid-poll and you burn rate limit for nothing.
  • lock_store must be shared across workers. array or file on a multi-worker setup silently defeats the in-flight lock and you're back to double-submits. Use redis or database.
  • Rate limiting is now shared per endpoint and client id. Previously each submission uid had its own key, so throughput was effectively per-submission. Now a burst of concurrent submissions contends for one bucket and you'll see 429s where you didn't before. Reduce concurrency or add backoff; don't just raise the limit.
  • Saving a document status no longer bumps the parent's updated_at. If any of your caching, ETags or "recently changed" queries keyed off that touch, they'll go stale. Add an explicit touch() in a DocumentStatusChanged listener where you actually need it — this is the one fix that can regress working code.
  • A response without status no longer blanks the row, and stored validation results are kept. If you had defensive code checking for null status transitions, it's now dead — remove it rather than leaving a wrong assumption in place.
  • unauthorized_client messaging changed to name the on-behalf-of TIN and the environment. Any string matching or error mapping on that code needs re-checking.
  • The queued payload is encrypted and carries no secrets. That means the worker must be able to resolve credentials on its own via clientFor() — if your resolver depends on request-scoped state, jobs will fail on the worker only, which is easy to miss locally where the sync driver runs inline.
  • sync_schedule being unset is silent. No schedule, no error. Add schedule:list to your deploy smoke checks.
  • Not verified live: INVALID reasons (unit-tested only), rejectDocument(), and payable_rounding. Exercise rejectDocument() on preprod yourself before depending on it, and eyeball rounding on any invoice where totals land on a half-cent.

Extension points worth knowing: ResolvesMyinvoisClient is the intended seam for multi-tenant credentials, and the three submission events are the clean place to hang notifications, audit logs and cache invalidation — resist reaching into the models from your controller instead.

Weaver

How can I help you explore Laravel packages today?

Conversation history is not saved when not logged in.
Prompt
Add packages to context
No packages found.
robertmarney/lara-hierarchial-collections
simonschaufi/rule-doc-generator
emissaoja/emissaoja
blackbricksoftware/gravityforms-civicrm-form-processor
al-rahabi/fancy-crud
jinas/dhivatar-component
mltstephane/laravel-quality
bernskiold/laravel-talentlms
bernskioldmedia/laravel-form-architect
youbar/easy-crud
c-point-labs/laravel-hostyourai
dbravoan/dbcms-admin-theme-dbcms
dbravoan/dbcms-theme-db-cyber
dbravoan/dbcms-blocks-basic
dbravoan/dbcms-module-content-audit
dbravoan/dbcms-module-ai-assistant
paygate-to/paygate-php
padosoft/askmydocs-connector-base
defstudio/supercharged-carbon
bernskioldmedia/laravel-matomo