/laravel-vouchers

Voucher system for Laravel 5.6+

Primary LanguagePHPMIT LicenseMIT

Laravel Vouchers

Packagist Downloads License GitHub Workflow Status

Table of Contents

Installation

Install this package via Composer:

$ composer require frittenkeez/laravel-vouchers

Upgrading

Please read the upgrade guide.

Changelog

Please read the release notes.

Configuration

Publish config using Artisan command:

$ php artisan vendor:publish --tag=config --provider="FrittenKeeZ\Vouchers\VouchersServiceProvider"

Don't forget to run migrations:

$ php artisan migrate

Change basic configuration through config/vouchers.php - it should be well documented, so no need to describe all options here.

Usage

This package comes with an ease-of-use facade Vouchers with FQN FrittenKeeZ\Vouchers\Facades\Vouchers.

Generate Codes

Generating codes without checking if they exist; defaults from config will be used if not specified.

Vouchers::generate(string|null $mask = null, string|null $characters = null): string;

$code = Vouchers::generate('***-***-***', '1234567890');

Batch generation of codes is also possible; these will be checked against existing codes.

Vouchers::batch(int amount): array;

$codes = Vouchers::batch(10);

Create Vouchers

Generating one or more vouchers is just as simple.

Vouchers::create(int $amount = 1): object|array;

$voucher = Vouchers::create();
$vouchers = Vouchers::create(10);

Redeem Vouchers

Redeeming vouchers requires that one provides a redeemer entity.
Additional metadata for the redeemer can be provided.

Vouchers::redeem(string $code, Illuminate\Database\Eloquent\Model $entity, array $metadata = []): bool;

try {
    $success = Vouchers::redeem('123-456-789', $user, ['foo' => 'bar']);
} catch (FrittenKeeZ\Vouchers\Exceptions\VoucherNotFoundException $e) {
    // Code provided did not match any vouchers in the database.
} catch (FrittenKeeZ\Vouchers\Exceptions\VoucherAlreadyRedeemedException $e) {
    // Voucher has already been redeemed.
}

Options

Besides defaults specified in config/vouchers.php, one can override options when generating codes or creating vouchers.
Following methods apply to Vouchers::generate(), Vouchers::batch() and Vouchers::create() calls.

// Override characters list.
Vouchers::withCharacters(string $characters);
// Override code mask.
Vouchers::withMask(string $mask);
// Override code prefix.
Vouchers::withPrefix(string $prefix);
// Disable code prefix.
Vouchers::withoutPrefix();
// Override code suffix.
Vouchers::withSuffix(string $suffix);
// Disable code suffix.
Vouchers::withoutSuffix();
// Override prefix and suffix separator.
Vouchers::withSeparator(string $separator);
// Disable prefix and suffix separator.
Vouchers::withoutSeparator();

Following methods only apply to Vouchers::create() call.

// Add metadata to voucher.
Vouchers::withMetadata(array $metadata);
// Set voucher start time.
Vouchers::withStartTime(DateTime $timestamp);
// Set voucher start time using interval.
Vouchers::withStartTimeIn(DateInterval $interval);
// Set voucher start date - time component is zeroed.
Vouchers::withStartDate(DateTime $timestamp);
// Set voucher start date using interval - time component is zeroed.
Vouchers::withStartDateIn(DateInterval $interval);
// Set voucher expire time.
Vouchers::withExpireTime(DateTime $timestamp);
// Set voucher expire time using interval.
Vouchers::withExpireTimeIn(DateInterval $interval);
// Set voucher expire date - time component is set to end of day (23:59:59).
Vouchers::withExpireDate(DateTime $timestamp);
// Set voucher expire date using interval - time component is set to end of day (23:59:59).
Vouchers::withExpireDateIn(DateInterval $interval);
// Set related entities to voucher.
Vouchers::withEntities(Illuminate\Database\Eloquent\Model ...$entities);
// Set owning entity for voucher.
Vouchers::withOwner(Illuminate\Database\Eloquent\Model $owner);

All calls are chainable and dynamic options will be reset when calling Vouchers::create() or Vouchers::reset().

$voucher = Vouchers::withMask('***-***-***')
    ->withMetadata(['foo' => 'bar'])
    ->withExpireDateIn(CarbonInterval::create('P30D'))
    ->create();
$voucher = Vouchers::withOwner($user)->withPrefix('USR');

Events

During events Voucher::$redeemer will be set to the active redeemer (FrittenKeeZ\Vouchers\Models\Redeemer).

By default vouchers will be marked as redeemed after one use, which is not always the desired outcome.
To allow a voucher to be redeemed multiple times, subscribe to the FrittenKeeZ\Vouchers\Models\Voucher::shouldMarkRedeemed() event.

Voucher::shouldMarkRedeemed(function (Voucher $voucher) {
    // Do some fancy checks here.
    return false;
});

To prevent a voucher from being redeemed altogether, subscribe to the FrittenKeeZ\Vouchers\Models\Voucher::redeeming() event.

Voucher::redeeming(function (Voucher $voucher) {
    // Do some fancy checks here.
    return false;
});

To prevent a voucher from being redeemed by anyone but the related user.

Voucher::redeeming(function (Voucher $voucher) {
    return $voucher->redeemer->redeemer->is($voucher->owner);
});
/* ... */
$voucher = Vouchers::withOwner($user)->create();
Vouchers::redeem($voucher->code, $user);

To perform additional actions after a vouchers has been redeemed, subscribe to the FrittenKeeZ\Vouchers\Models\Voucher::redeemed() event.

Voucher::redeemed(function (Voucher $voucher) {
    // Do some additional stuff here.
});

Traits

For convenience we provide some traits for fetching vouchers and redeemers for related entities, fx. users.
FrittenKeeZ\Vouchers\Concerns\HasRedeemers

// Associated redeemers relationship.
HasRedeemers::redeemers(): MorphMany;
// Get all associated redeemers.
$redeemers = $user->redeemers;

FrittenKeeZ\Vouchers\Concerns\HasVouchers

// Owned vouchers relationship.
HasVouchers::vouchers(): MorphMany;
// Get all owned vouchers.
$vouchers = $user->vouchers;

// Associated vouchers relationship.
HasVouchers::associatedVouchers(): MorphToMany;
// Get all associated vouchers.
$vouchers = $user->associatedVouchers;

// Associated voucher entities relationship.
HasVouchers::voucherEntities(): MorphMany;
// Get all associated voucher entities.
$entities = $user->voucherEntities;

You can also create vouchers owned by an entity using these convenience methods.

HasVouchers::createVoucher(Closure|null $callback = null): object;

// Without using callback.
$voucher = $user->createVoucher();
// With using callback.
$voucher = $user->createVoucher(function (FrittenKeeZ\Vouchers\Vouchers $vouchers) {
    $vouchers->withPrefix('USR');
});

HasVouchers::createVouchers(int $amount, Closure|null $callback = null): object|array;

// Without using callback.
$vouchers = $user->createVouchers(3);
// With using callback.
$vouchers = $user->createVouchers(3, function (FrittenKeeZ\Vouchers\Vouchers $vouchers) {
    $vouchers->withPrefix('USR');
});

Helpers

Check whether a voucher code is redeemable without throwing any errors.

Vouchers::redeemable(string $code, Closure|null $callback = null): bool;

// Without using callback.
$valid = Vouchers::redeemable('123-456-789');
// With using callback.
$valid = Vouchers::redeemable('123-456-789', function (FrittenKeeZ\Vouchers\Models\Voucher $voucher) {
    return $voucher->hasPrefix('foo');
});

Check whether a voucher code exists, optionally also checking a provided list.

Vouchers::exists(string $code, array $codes = []): bool;

$exists = Vouchers::exists('123-456-789', ['987-654-321']);

Additional helpers methods on Voucher model.

// Whether voucher has prefix, optionally specifying a separator different from config.
Voucher::hasPrefix(string $prefix, string|null $separator = null): bool;
// Whether voucher has suffix, optionally specifying a separator different from config.
Voucher::hasSuffix(string $suffix, string|null $separator = null): bool;
// Whether voucher is started.
Voucher::isStarted(): bool;
// Whether voucher is expired.
Voucher::isExpired(): bool;
// Whether voucher is redeemed.
Voucher::isRedeemed(): bool;
// Whether voucher is redeemable.
Voucher::isRedeemable(): bool;

Scopes

For convenience we also provide Voucher scopes matching the helper methods.

// Scope voucher query to a specific prefix, optionally specifying a separator different from config.
Voucher::withPrefix(string $prefix, string|null $separator = null);
// Scope voucher query to exclude a specific prefix, optionally specifying a separator different from config.
Voucher::withoutPrefix(string $prefix, string|null $separator = null);
// Scope voucher query to a specific suffix, optionally specifying a separator different from config.
Voucher::withSuffix(string $suffix, string|null $separator = null);
// Scope voucher query to exclude a specific suffix, optionally specifying a separator different from config.
Voucher::withoutSuffix(string $suffix, string|null $separator = null);
// Scope voucher query to started vouchers.
Voucher::withStarted();
// Scope voucher query to unstarted vouchers.
Voucher::withoutStarted();
// Scope voucher query to expired vouchers.
Voucher::withExpired();
// Scope voucher query to unexpired vouchers.
Voucher::withoutExpired();
// Scope voucher query to redeemed vouchers.
Voucher::withRedeemed();
// Scope voucher query to unredeemed vouchers.
Voucher::withoutRedeemed();
// Scope voucher query to redeemable vouchers.
Voucher::withRedeemable();
// Scope voucher query to unredeemable vouchers.
Voucher::withoutRedeemable();
// Scope voucher query to have voucher entities, optionally of a specific type (class or alias).
Voucher::withEntities(string|null $type = null);
// Scope voucher query to specific owner type (class or alias).
Voucher::withOwnerType(string $type);
// Scope voucher query to specific owner.
Voucher::withOwner(Illuminate\Database\Eloquent\Model $owner);
// Scope voucher query to no owners.
Voucher::withoutOwner();

Testing

Running tests can be done either through composer, or directly calling the PHPUnit binary.

$ composer test

To run tests with code coverage, please make sure that phpdbg exists and is executable.

$ composer test-coverage
$ open tests/_reports/index.html

License

The MIT License (MIT). Please see License File for more information.