Installation Add the bundle via Composer:
composer require atm/pointsbundle
Register the bundle in config/app.php under providers:
Atm\PointsBundle\PointsBundle::class,
Publish the bundle’s configuration:
php artisan vendor:publish --provider="Atm\PointsBundle\PointsBundle" --tag=config
Basic Setup
config/points.php (e.g., default_points, expiration_days).purchase, referral) in the database via migrations or seeders.php artisan migrate
First Use Case: Awarding Points
Use the PointsService to award points to a user:
use Atm\PointsBundle\Services\PointsService;
$pointsService = app(PointsService::class);
$pointsService->awardPoints(
userId: 1,
type: 'purchase',
amount: 100,
metadata: ['order_id' => 123]
);
Awarding Points
PointsService::awardPoints() for dynamic point allocations.event(new UserAwardedPoints($user, $type, $amount));
Redeeming Points
RedeemPointsRequest with business logic (e.g., minimum balance checks):
$redeemed = $pointsService->redeemPoints(
userId: 1,
amount: 50,
redeemableId: 'discount_123'
);
Expiration Handling
PointsService::expirePoints():
* * * * * php artisan atm:points:expire
Integration with Events
PointsAwarded or PointsRedeemed events to trigger side effects (e.g., notifications):
public function handle(PointsAwarded $event) {
Notification::send($event->user, new PointsNotification($event->amount));
}
PointType model or use traits for domain-specific logic.PointsService::batchAward() for bulk operations (e.g., promotions):
$pointsService->batchAward([
['user_id' => 1, 'type' => 'promo', 'amount' => 200],
['user_id' => 2, 'type' => 'promo', 'amount' => 200],
]);
public function award(Request $request) {
$this->pointsService->awardPoints($request->user(), $request->type, $request->amount);
return response()->json(['success' => true]);
}
Transaction Management
DB::transaction(function () use ($pointsService) {
$pointsService->awardPoints(...);
// Other related DB operations
});
Expiration Logic
expiration_days in config aligns with business needs (e.g., null for never-expiring points).Metadata Serialization
metadata column. Use json_encode()/json_decode() carefully to avoid type issues.Race Conditions
selectForUpdate() for critical operations (e.g., redeeming points):
$userPoints = UserPoints::where('user_id', $userId)->lockForUpdate()->first();
config/points.php to track awards/redemptions:
'logging' => [
'enabled' => true,
'channel' => 'points',
],
UserPoints queries.max_points_per_type.Custom Validators
Override Atm\PointsBundle\Validators\PointsValidator to add rules (e.g., blacklisted users):
public function validateAward($user, $type, $amount) {
if ($user->isBlacklisted()) {
throw new \Exception("Blacklisted users cannot earn points.");
}
parent::validateAward($user, $type, $amount);
}
Event Listeners
Extend the bundle’s events (e.g., PointsAwarded) to integrate with third-party services:
public function handle(PointsAwarded $event) {
Analytics::track($event->user, 'points_awarded', ['type' => $event->type]);
}
Custom Storage
Replace the default UserPoints model with a trait or interface for alternative storage (e.g., Redis):
class RedisUserPoints extends UserPoints {
use \Atm\PointsBundle\Traits\RedisPointsStorage;
}
How can I help you explore Laravel packages today?