Service row, and your module makes the real thing exist.
Read Fundamentals first if you haven’t.
Generate it
What you get
What you get
TODO and you’ll find the four or five places that need your API calls.
What core calls, and when
Provisioning
The one class you can’t do without. It runs in a queued job after the payment clears.app/Services/ModuleServiceActions.php
Suspend, unsuspend, terminate
Three static methods, each taking a service ID rather than a model.app/Services/Actions/SuspendServerService.php
Renewal
BaseRenewService already owns the billing side: billing cycles, configurable option pricing, due
dates, free trial conversion, admin suspension checks. You only handle the side effect.
app/Services/RenewService.php
Cancellation
BaseCancelService handles the awkward parts. You supply two methods.
app/Services/CancelService.php
Pending services cancel cleanly
Pending services cancel cleanly
If there’s no server ID and the service is
pending, it’s marked terminated and the cancellation
succeeds. Provisioning failed, so there’s nothing to delete.Without this a customer can’t cancel a service that never worked.Active services with a missing ID throw
Active services with a missing ID throw
A missing ID on an active service is a data problem somebody needs to look at, not something to
quietly terminate.
Failures leave the service alone
Failures leave the service alone
So it can be retried, rather than ending up terminated locally with a live server.
Password verification is handled
Password verification is handled
It reads the raw attribute, because
$user->password can be cast or hidden.Checkout fields
Optional. Delete the class if your product needs no customer input.
app/Services/MarketCheckoutConfig.php
validateCheckoutData() has to return exactly ['validated' => [...], 'errors' => [...]], because
core reads both keys by name. The generator writes a working version.
Customer panel
Optional.
This is only called when the service is
active, and you must accept both arguments
($serviceConfig, $serviceId) even if you ignore them. Core always passes them.Upgrades, downgrades, addons
Optional. All three follow the same pattern.
true and core renames the service, marks the invoice processed and notifies the customer.
Return false and none of that happens, so the customer keeps their old resources. That’s the right
failure mode.
processDowngrade() is identical. AddonService has applyAddon() and removeAddon(), both taking
(Service $service, array $addon, int $quantity = 1).
Test it properly
1
Buy it
Create a product bound to your module and buy it as a test user.
2
Check the remote side
The server exists and
Service.config has its ID.3
Run the lifecycle
Suspend, unsuspend, renew, cancel from admin. Check the remote state each time.
4
Re-run provisioning
Run the job again for the same service. Confirm you don’t get a second server.
5
Break it on purpose
Wreck the credentials. Confirm the failure is loud, retried and visible to an admin.
6
Delete the server behind DezerX's back
Then terminate the service. It should still work.
Worth reading
CPanelService
Clearest small implementation.
LicenseService
No external panel at all.
VirtfusionService
VPS lifecycle with addons.
PterodactylService
The big one: per-hour billing, auto-stock, file manager.

