mirror of
https://github.com/OPSnet/Gazelle.git
synced 2026-01-16 18:04:34 -05:00
promote bonus shop items to first class objects
This commit is contained in:
345
app/BonusItem.php
Normal file
345
app/BonusItem.php
Normal file
@@ -0,0 +1,345 @@
|
||||
<?php
|
||||
|
||||
namespace Gazelle;
|
||||
|
||||
class BonusItem extends \Gazelle\BaseObject {
|
||||
final public const tableName = 'bonus_item';
|
||||
final public const CACHE_KEY = 'bonus_%d';
|
||||
|
||||
public function flush(): static {
|
||||
self::$cache->delete_value(sprintf(self::CACHE_KEY, $this->id));
|
||||
unset($this->info);
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function link(): string {
|
||||
return '';
|
||||
}
|
||||
|
||||
public function location(): string {
|
||||
return 'bonus.php';
|
||||
}
|
||||
|
||||
public function info(): array {
|
||||
if (isset($this->info)) {
|
||||
return $this->info;
|
||||
}
|
||||
$key = sprintf(self::CACHE_KEY, $this->id);
|
||||
$info = self::$cache->get_value($key);
|
||||
if ($info === false) {
|
||||
$info = $this->pg()->rowAssoc('
|
||||
select price,
|
||||
amount,
|
||||
userclass_min,
|
||||
userclass_free,
|
||||
prepare,
|
||||
frequency,
|
||||
label,
|
||||
title
|
||||
from bonus_item
|
||||
where id_bonus_item = ?
|
||||
', $this->id
|
||||
);
|
||||
self::$cache->cache_value($key, $info, 0);
|
||||
}
|
||||
$this->info = $info;
|
||||
return $this->info;
|
||||
}
|
||||
|
||||
public function amount(): int {
|
||||
return $this->info()['amount'];
|
||||
}
|
||||
|
||||
public function label(): string {
|
||||
return $this->info()['label'];
|
||||
}
|
||||
|
||||
public function needsPreparation(): bool {
|
||||
return $this->info()['prepare'];
|
||||
}
|
||||
|
||||
public function isRecurring(): bool {
|
||||
return $this->info()['frequency'] === 'recurring';
|
||||
}
|
||||
|
||||
/**
|
||||
* Even after userclass levels are taken into account, there may exist other
|
||||
* reasons to deny a user from buying an item from the shop.
|
||||
*/
|
||||
public function permitted(User $user): bool {
|
||||
return match ($this->label()) {
|
||||
'invite' => !$user->disableInvites(),
|
||||
default => true,
|
||||
};
|
||||
}
|
||||
|
||||
public function price(): int {
|
||||
return $this->info()['price'];
|
||||
}
|
||||
|
||||
public function priceForUser(User $user): int {
|
||||
return match ($this->label()) {
|
||||
'collage-1' => $this->price() * 2 ** min(6, $user->paidPersonalCollages()),
|
||||
default => $user->classLevel() >= $this->userclassFree()
|
||||
? 0 : $this->price(),
|
||||
};
|
||||
}
|
||||
|
||||
public function title(): string {
|
||||
return $this->info()['title'];
|
||||
}
|
||||
|
||||
public function userclassFree(): int {
|
||||
return $this->info()['userclass_free'];
|
||||
}
|
||||
|
||||
public function userclassMin(): int {
|
||||
return $this->info()['userclass_min'];
|
||||
}
|
||||
|
||||
public function isPurchased(User $user): bool {
|
||||
return (bool)$this->pg()->scalar('
|
||||
select exists (
|
||||
select 1
|
||||
from relay.bonus_history bh
|
||||
where bh."ItemID" = ?
|
||||
and bh."UserID" = ?
|
||||
)
|
||||
', $this->id, $user->id
|
||||
);
|
||||
}
|
||||
|
||||
/* Object-orient orthodoxy would suggest creating classes for each type
|
||||
* of item that can be purchased, and calling the purchase() method
|
||||
* through polymorphism. That would be a lot of boilerplate code simply
|
||||
* to provide one method. Rules are made to be broken, so we will cheat
|
||||
* and use a humongous switch. New items will need their own effect
|
||||
* added here. An unintended benefit is that determining the price and
|
||||
* recording the purchase history happen in one place.
|
||||
*
|
||||
* Note: the price of the item is passed in as a parameter. This may
|
||||
* seem a little weird, because the method call looks something like:
|
||||
* $item->purchase($user, $item->price());
|
||||
* ... but it allows the gifting of something to a user for free:
|
||||
* $item->purchase($user, 0);
|
||||
*/
|
||||
public function purchase(User $user, int $price, array $extra = []): Enum\BonusItemPurchaseStatus {
|
||||
if ($user->classLevel() < $this->userclassMin()) {
|
||||
return Enum\BonusItemPurchaseStatus::forbidden;
|
||||
}
|
||||
if ($this->isPurchased($user) && !$this->isRecurring()) {
|
||||
return Enum\BonusItemPurchaseStatus::alreadyPurchased;
|
||||
}
|
||||
if ($user->classLevel() >= $this->userclassFree()) {
|
||||
$price = 0;
|
||||
}
|
||||
|
||||
if (str_starts_with($this->label(), 'token-')) {
|
||||
self::$db->prepared_query('
|
||||
UPDATE user_bonus ub
|
||||
INNER JOIN user_flt uf USING (user_id) SET
|
||||
uf.tokens = uf.tokens + ?,
|
||||
ub.points = ub.points - ?
|
||||
WHERE ub.points >= ?
|
||||
AND ub.user_id = ?
|
||||
', $this->amount(), $price, $price, $user->id
|
||||
);
|
||||
if (self::$db->affected_rows() != 2) {
|
||||
return Enum\BonusItemPurchaseStatus::insufficientFunds;
|
||||
}
|
||||
} elseif (str_starts_with($this->label(), 'other-')) {
|
||||
if (!isset($extra['receiver'])) {
|
||||
return Enum\BonusItemPurchaseStatus::incomplete;
|
||||
}
|
||||
$receiver = $extra['receiver'];
|
||||
if ($user->id === $receiver->id || !$receiver->isEnabled()) {
|
||||
return Enum\BonusItemPurchaseStatus::forbidden;
|
||||
}
|
||||
if ($receiver->hasAttr('no-fl-gifts')) {
|
||||
return Enum\BonusItemPurchaseStatus::declined;
|
||||
}
|
||||
/* Take the bonus points from the giver and give tokens to the
|
||||
* receiver, unless the latter have asked to refuse receiving
|
||||
* tokens. Yes, we have just checked this and possibly sent a
|
||||
* 'declined' response. But race conditions being what they
|
||||
* are, the proof is in the database update. If this fails,
|
||||
* we will consider it is due to insufficient funds (the more
|
||||
* likely scenario anyway) and the purchaser can try again.
|
||||
*/
|
||||
self::$db->prepared_query("
|
||||
UPDATE user_bonus ub
|
||||
INNER JOIN users_main self ON (self.ID = ub.user_id),
|
||||
users_main other
|
||||
INNER JOIN user_flt other_uf ON (other_uf.user_id = other.ID)
|
||||
LEFT JOIN user_has_attr noFL ON (noFL.UserID = other.ID AND noFL.UserAttrId
|
||||
= (SELECT ua.ID FROM user_attr ua WHERE ua.Name = 'no-fl-gifts')
|
||||
)
|
||||
SET
|
||||
other_uf.tokens = other_uf.tokens + ?,
|
||||
ub.points = ub.points - ?
|
||||
WHERE noFL.UserID IS NULL
|
||||
AND ub.points >= ?
|
||||
AND other.Enabled = '1'
|
||||
AND other.ID = ?
|
||||
AND self.ID = ?
|
||||
", $this->amount(), $price, $price, $receiver->id, $user->id
|
||||
);
|
||||
if (self::$db->affected_rows() != 2) {
|
||||
return Enum\BonusItemPurchaseStatus::insufficientFunds;
|
||||
}
|
||||
$amount = $this->amount();
|
||||
$receiver->inbox()->createSystem(
|
||||
"Here " . ($amount == 1 ? 'is' : 'are') . ' ' . article($amount)
|
||||
. " freeleech token" . plural($amount) . "!",
|
||||
self::$twig->render('bonus/token-other-message.bbcode.twig', [
|
||||
'amount' => $amount,
|
||||
'from' => $user->username(),
|
||||
'to' => $receiver->username(),
|
||||
'message' => $extra['message'] ?? '',
|
||||
'wiki_id' => 57,
|
||||
])
|
||||
);
|
||||
} else {
|
||||
switch ($this->label()) {
|
||||
case 'collage-1':
|
||||
$price = (int)($price * 2 ** $user->paidPersonalCollages());
|
||||
self::$db->begin_transaction();
|
||||
self::$db->prepared_query('
|
||||
UPDATE user_bonus ub
|
||||
INNER JOIN users_main um ON (um.ID = ub.user_id) SET
|
||||
ub.points = ub.points - ?,
|
||||
um.collage_total = um.collage_total + 1
|
||||
WHERE ub.points >= ?
|
||||
AND ub.user_id = ?
|
||||
', $price, $price, $user->id
|
||||
);
|
||||
$rows = self::$db->affected_rows();
|
||||
if (($price > 0 && $rows !== 2) || ($price === 0 && $rows !== 1)) {
|
||||
self::$db->rollback();
|
||||
return Enum\BonusItemPurchaseStatus::insufficientFunds;
|
||||
}
|
||||
$user->ordinal()
|
||||
->set(
|
||||
'personal-collage',
|
||||
(int)self::$db->scalar("
|
||||
SELECT collage_total FROM users_main WHERE ID = ?
|
||||
", $user->id
|
||||
)
|
||||
);
|
||||
self::$db->commit();
|
||||
break;
|
||||
|
||||
case 'file-count':
|
||||
self::$db->begin_transaction();
|
||||
self::$db->prepared_query('
|
||||
UPDATE user_bonus ub SET
|
||||
ub.points = ub.points - ?
|
||||
WHERE ub.points >= ?
|
||||
AND ub.user_id = ?
|
||||
', $price, $price, $user->id
|
||||
);
|
||||
if (self::$db->affected_rows() != 1) {
|
||||
self::$db->rollback();
|
||||
return Enum\BonusItemPurchaseStatus::insufficientFunds;
|
||||
}
|
||||
try {
|
||||
self::$db->prepared_query("
|
||||
INSERT INTO user_has_attr
|
||||
(UserID, UserAttrID)
|
||||
VALUES (?, (SELECT ID FROM user_attr WHERE Name = ?))
|
||||
", $user->id, 'feature-file-count'
|
||||
);
|
||||
} catch (\Gazelle\DB\MysqlDuplicateKeyException) {
|
||||
// no point in buying a second time
|
||||
self::$db->rollback();
|
||||
return Enum\BonusItemPurchaseStatus::alreadyPurchased;
|
||||
}
|
||||
self::$db->commit();
|
||||
break;
|
||||
|
||||
case 'invite':
|
||||
if (!$user->canPurchaseInvite()) {
|
||||
return Enum\BonusItemPurchaseStatus::forbidden;
|
||||
}
|
||||
self::$db->prepared_query('
|
||||
UPDATE user_bonus ub
|
||||
INNER JOIN users_main um ON (um.ID = ub.user_id) SET
|
||||
ub.points = ub.points - ?,
|
||||
um.Invites = um.Invites + 1
|
||||
WHERE ub.points >= ?
|
||||
AND ub.user_id = ?
|
||||
', $price, $price, $user->id
|
||||
);
|
||||
if (self::$db->affected_rows() != 2) {
|
||||
return Enum\BonusItemPurchaseStatus::insufficientFunds;
|
||||
}
|
||||
break;
|
||||
|
||||
case 'seedbox':
|
||||
self::$db->begin_transaction();
|
||||
self::$db->prepared_query('
|
||||
UPDATE user_bonus ub SET
|
||||
ub.points = ub.points - ?
|
||||
WHERE ub.points >= ?
|
||||
AND ub.user_id = ?
|
||||
', $price, $price, $user->id
|
||||
);
|
||||
if (self::$db->affected_rows() != 1) {
|
||||
self::$db->rollback();
|
||||
return Enum\BonusItemPurchaseStatus::insufficientFunds;
|
||||
}
|
||||
try {
|
||||
self::$db->prepared_query("
|
||||
INSERT INTO user_has_attr
|
||||
(UserID, UserAttrID)
|
||||
VALUES (?, (SELECT ID FROM user_attr WHERE Name = ?))
|
||||
", $user->id, 'feature-seedbox'
|
||||
);
|
||||
} catch (DB\MysqlDuplicateKeyException) {
|
||||
// no point in buying a second time
|
||||
self::$db->rollback();
|
||||
return Enum\BonusItemPurchaseStatus::alreadyPurchased;
|
||||
}
|
||||
self::$db->commit();
|
||||
break;
|
||||
|
||||
case 'title-bb-n':
|
||||
case 'title-bb-y':
|
||||
if (!isset($extra['title'])) {
|
||||
return Enum\BonusItemPurchaseStatus::incomplete;
|
||||
}
|
||||
if ($price > 0 && !new User\Bonus($user)->removePoints($price)) {
|
||||
return Enum\BonusItemPurchaseStatus::insufficientFunds;
|
||||
}
|
||||
if (
|
||||
!$user->setTitle(
|
||||
$this->label() === 'title-bb-y'
|
||||
? \Text::span_format($extra['title'])
|
||||
: \Text::strip_bbcode($extra['title'])
|
||||
)
|
||||
) {
|
||||
return Enum\BonusItemPurchaseStatus::incomplete;
|
||||
}
|
||||
$user->modify();
|
||||
break;
|
||||
|
||||
case 'title-off':
|
||||
$user->removeTitle()->modify();
|
||||
break;
|
||||
}
|
||||
}
|
||||
self::$db->prepared_query("
|
||||
INSERT INTO bonus_history
|
||||
(ItemID, UserID, Price, OtherUserID)
|
||||
VALUES (?, ?, ?, ?)
|
||||
", $this->id,
|
||||
$user->id,
|
||||
$price,
|
||||
isset($extra['receiver']) ? $extra['receiver']->id : 0,
|
||||
);
|
||||
new User\Bonus($user)->flush();
|
||||
$user->flush();
|
||||
|
||||
return Enum\BonusItemPurchaseStatus::success;
|
||||
}
|
||||
}
|
||||
12
app/Enum/BonusItemPurchaseStatus.php
Normal file
12
app/Enum/BonusItemPurchaseStatus.php
Normal file
@@ -0,0 +1,12 @@
|
||||
<?php
|
||||
|
||||
namespace Gazelle\Enum;
|
||||
|
||||
enum BonusItemPurchaseStatus {
|
||||
case success;
|
||||
case insufficientFunds;
|
||||
case declined;
|
||||
case forbidden;
|
||||
case alreadyPurchased;
|
||||
case incomplete;
|
||||
}
|
||||
25
app/Json/BonusItemTitle.php
Normal file
25
app/Json/BonusItemTitle.php
Normal file
@@ -0,0 +1,25 @@
|
||||
<?php
|
||||
|
||||
namespace Gazelle\Json;
|
||||
|
||||
use Gazelle\Manager\Bonus as BonusManager;
|
||||
|
||||
class BonusItemTitle extends \Gazelle\Json {
|
||||
public function __construct(
|
||||
protected string $label,
|
||||
protected string $title,
|
||||
protected BonusManager $manager = new BonusManager(),
|
||||
) {}
|
||||
|
||||
public function payload(): array {
|
||||
$item = $this->manager->findBonusItemByLabel($this->label);
|
||||
if (is_null($item)) {
|
||||
return [];
|
||||
}
|
||||
return [
|
||||
$item->label() === 'title-bb-y'
|
||||
? \Text::full_format($this->title)
|
||||
: \Text::strip_bbcode($this->title)
|
||||
];
|
||||
}
|
||||
}
|
||||
28
app/Json/BonusUserOther.php
Normal file
28
app/Json/BonusUserOther.php
Normal file
@@ -0,0 +1,28 @@
|
||||
<?php
|
||||
|
||||
namespace Gazelle\Json;
|
||||
|
||||
use Gazelle\Manager\User as UserManager;
|
||||
|
||||
class BonusUserOther extends \Gazelle\Json {
|
||||
public function __construct(
|
||||
protected string $username,
|
||||
protected UserManager $manager = new UserManager(),
|
||||
) {}
|
||||
|
||||
public function payload(): array {
|
||||
$other = $this->manager->findByUsername($this->username);
|
||||
return is_null($other)
|
||||
? [
|
||||
'found' => false,
|
||||
'username' => $this->username,
|
||||
]
|
||||
: [
|
||||
'found' => true,
|
||||
'accept' => !$other->hasAttr('no-fl-gifts'),
|
||||
'enabled' => $other->isEnabled(),
|
||||
'id' => $other->id,
|
||||
'username' => $other->username(),
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -1,15 +1,37 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Gazelle\Manager;
|
||||
|
||||
use Gazelle\BonusItem;
|
||||
use Gazelle\Enum\BonusItemPurchaseStatus;
|
||||
use Gazelle\Enum\UserStatus;
|
||||
use Gazelle\User;
|
||||
|
||||
class Bonus extends \Gazelle\Base {
|
||||
final public const CACHE_OPEN_POOL = 'bonus_pool'; // also defined in \Gazelle\Bonus
|
||||
final protected const CACHE_ITEM = 'bonus_item';
|
||||
final protected const CACHE_KEY = 'bonus_item_list';
|
||||
|
||||
protected array $items;
|
||||
|
||||
public function flush(): static {
|
||||
foreach ($this->itemList() as $item) {
|
||||
$item->flush();
|
||||
}
|
||||
unset($this->items);
|
||||
self::$cache->delete_value(self::CACHE_KEY);
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function findBonusItemByLabel(string $label): ?BonusItem {
|
||||
$idBonusItem = $this->pg()->scalar('
|
||||
select id_bonus_item from bonus_item where label = ?
|
||||
', $label
|
||||
);
|
||||
return is_null($idBonusItem) ? null : new BonusItem($idBonusItem);
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the global discount rate for the shop
|
||||
*
|
||||
@@ -21,36 +43,23 @@ class Bonus extends \Gazelle\Base {
|
||||
|
||||
public function itemList(): array {
|
||||
if (!isset($this->items)) {
|
||||
$items = self::$cache->get_value(self::CACHE_ITEM);
|
||||
if ($items === false) {
|
||||
$discount = $this->discount();
|
||||
self::$db->prepared_query("
|
||||
SELECT ID,
|
||||
Price * (greatest(0, least(100, 100 - ?)) / 100) as Price,
|
||||
Amount, MinClass, FreeClass, Label, Title, sequence,
|
||||
IF (Label REGEXP '^other-', 'NoOp', 'ConfirmPurchase') AS JS_on_click,
|
||||
IF (Label REGEXP '^title-bb-[yn]', 'NoOp', 'ConfirmPurchase') AS JS_on_click
|
||||
FROM bonus_item
|
||||
ORDER BY sequence
|
||||
", $discount
|
||||
);
|
||||
$items = [];
|
||||
foreach (self::$db->to_array(false, MYSQLI_ASSOC) as $row) {
|
||||
$row['Price'] = (int)$row['Price'];
|
||||
$items[$row['Label']] = $row;
|
||||
}
|
||||
self::$cache->cache_value(self::CACHE_ITEM, $items, 0);
|
||||
$idList = self::$cache->get_value(self::CACHE_KEY);
|
||||
if ($idList === false) {
|
||||
$idList = $this->pg()->column("
|
||||
select id_bonus_item from bonus_item order by sequence
|
||||
");
|
||||
self::$cache->cache_value(self::CACHE_KEY, $idList, 0);
|
||||
}
|
||||
$this->items = $items;
|
||||
$list = [];
|
||||
foreach ($idList as $idBonusItem) {
|
||||
$item = new BonusItem($idBonusItem);
|
||||
$list[$item->label()] = $item;
|
||||
}
|
||||
$this->items = $list;
|
||||
}
|
||||
return $this->items;
|
||||
}
|
||||
|
||||
public function flushPriceCache(): void {
|
||||
unset($this->items);
|
||||
self::$cache->delete_value(self::CACHE_ITEM);
|
||||
}
|
||||
|
||||
public function openPoolList(): array {
|
||||
$key = self::CACHE_OPEN_POOL;
|
||||
$pool = self::$cache->get_value($key);
|
||||
@@ -219,4 +228,38 @@ class Bonus extends \Gazelle\Base {
|
||||
|
||||
return $processed;
|
||||
}
|
||||
|
||||
/**
|
||||
* return array<BonusItem>
|
||||
*/
|
||||
public function offerTokenOther(User $user): array {
|
||||
$balance = $user->bonusPointsTotal();
|
||||
$offer = [];
|
||||
foreach ($this->itemList() as $item) {
|
||||
if (str_starts_with($item->label(), 'other-') && $balance >= $item->price()) {
|
||||
$offer[] = $item;
|
||||
}
|
||||
}
|
||||
return $offer;
|
||||
}
|
||||
|
||||
public function purchaseTokenOther(
|
||||
User $giver,
|
||||
User $receiver,
|
||||
string $label,
|
||||
string $message,
|
||||
): BonusItemPurchaseStatus {
|
||||
$item = $this->findBonusItemByLabel($label);
|
||||
if (is_null($item)) {
|
||||
return BonusItemPurchaseStatus::incomplete;
|
||||
}
|
||||
return $item->purchase(
|
||||
$giver,
|
||||
$item->price(),
|
||||
[
|
||||
'message' => $message,
|
||||
'receiver' => $receiver,
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
namespace Gazelle\User;
|
||||
|
||||
use Gazelle\BonusItem;
|
||||
use Gazelle\BonusPool;
|
||||
use Gazelle\Util\SortableTableHeader;
|
||||
|
||||
@@ -26,6 +27,8 @@ class Bonus extends \Gazelle\BaseUser {
|
||||
self::$cache->delete_multi([
|
||||
sprintf(self::CACHE_HISTORY, $this->user->id, 0),
|
||||
sprintf(self::CACHE_POOL_HISTORY, $this->user->id),
|
||||
sprintf(self::CACHE_PURCHASE, $this->user->id),
|
||||
sprintf(self::CACHE_SUMMARY, $this->user->id),
|
||||
]);
|
||||
return $this;
|
||||
}
|
||||
@@ -37,59 +40,6 @@ class Bonus extends \Gazelle\BaseUser {
|
||||
);
|
||||
}
|
||||
|
||||
protected function items(): array {
|
||||
return new \Gazelle\Manager\Bonus()->itemList();
|
||||
}
|
||||
|
||||
public function itemList(): array {
|
||||
$items = $this->items();
|
||||
$allowed = [];
|
||||
foreach ($items as $item) {
|
||||
if ($item['Label'] === 'seedbox' && $this->user->hasAttr('feature-seedbox')) {
|
||||
continue;
|
||||
} elseif ($item['Label'] === 'file-count' && $this->user->hasAttr('feature-file-count')) {
|
||||
continue;
|
||||
} elseif (
|
||||
$item['Label'] === 'invite'
|
||||
&& ($this->user->permitted('site_send_unlimited_invites') || !$this->user->canPurchaseInvite())
|
||||
) {
|
||||
continue;
|
||||
}
|
||||
$item['Price'] = $this->effectivePrice($item['Label']);
|
||||
$allowed[] = $item;
|
||||
}
|
||||
return $allowed;
|
||||
}
|
||||
|
||||
public function item(string $label): ?array {
|
||||
$items = $this->items();
|
||||
return $items[$label] ?? null;
|
||||
}
|
||||
|
||||
public function effectivePrice(string $label): int {
|
||||
$item = $this->items()[$label];
|
||||
if (preg_match('/^collage-\d$/', $label)) {
|
||||
return (int)($item['Price'] * 2 ** $this->user->paidPersonalCollages());
|
||||
}
|
||||
return $this->user->privilege()->effectiveClassLevel() >= $item['FreeClass'] ? 0 : (int)$item['Price'];
|
||||
}
|
||||
|
||||
public function otherList(): array {
|
||||
$balance = $this->user->bonusPointsTotal();
|
||||
$other = [];
|
||||
foreach ($this->items() as $label => $item) {
|
||||
if (preg_match('/^other-\d$/', (string)$label) && $balance >= $item['Price']) {
|
||||
$other[] = [
|
||||
'Label' => $item['Label'],
|
||||
'Name' => $item['Title'],
|
||||
'Price' => $item['Price'],
|
||||
'After' => $balance - $item['Price'],
|
||||
];
|
||||
}
|
||||
}
|
||||
return $other;
|
||||
}
|
||||
|
||||
public function otherLatest(\Gazelle\User $other): array {
|
||||
return self::$db->rowAssoc("
|
||||
SELECT bi.Title AS title,
|
||||
@@ -223,246 +173,6 @@ class Bonus extends \Gazelle\BaseUser {
|
||||
return $history;
|
||||
}
|
||||
|
||||
public function purchaseInvite(): bool {
|
||||
if (!$this->user->canPurchaseInvite()) {
|
||||
return false;
|
||||
}
|
||||
$item = $this->items()['invite'];
|
||||
$price = $item['Price'];
|
||||
self::$db->prepared_query('
|
||||
UPDATE user_bonus ub
|
||||
INNER JOIN users_main um ON (um.ID = ub.user_id) SET
|
||||
ub.points = ub.points - ?,
|
||||
um.Invites = um.Invites + 1
|
||||
WHERE ub.points >= ?
|
||||
AND ub.user_id = ?
|
||||
', $price, $price, $this->user->id
|
||||
);
|
||||
if (self::$db->affected_rows() != 2) {
|
||||
return false;
|
||||
}
|
||||
$this->addPurchaseHistory($item['ID'], $price);
|
||||
$this->flush();
|
||||
return true;
|
||||
}
|
||||
|
||||
public function purchaseTitle(string $label, string $title): bool {
|
||||
$item = $this->items()[$label];
|
||||
$title = $label === 'title-bb-y' ? \Text::span_format($title) : \Text::strip_bbcode($title);
|
||||
|
||||
if (!$this->user->setTitle($title)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
/* if the price is 0, nothing changes so avoid hitting the db */
|
||||
$price = $this->effectivePrice($label);
|
||||
if ($price > 0) {
|
||||
if (!$this->removePoints($price)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
$this->user->modify();
|
||||
$this->addPurchaseHistory($item['ID'], $price);
|
||||
$this->flush();
|
||||
return true;
|
||||
}
|
||||
|
||||
public function purchaseCollage(string $label): bool {
|
||||
$item = $this->items()[$label];
|
||||
$price = $this->effectivePrice($label);
|
||||
self::$db->prepared_query('
|
||||
UPDATE user_bonus ub
|
||||
INNER JOIN users_main um ON (um.ID = ub.user_id) SET
|
||||
ub.points = ub.points - ?,
|
||||
um.collage_total = um.collage_total + 1
|
||||
WHERE ub.points >= ?
|
||||
AND ub.user_id = ?
|
||||
', $price, $price, $this->user->id
|
||||
);
|
||||
$rows = self::$db->affected_rows();
|
||||
if (($price > 0 && $rows !== 2) || ($price === 0 && $rows !== 1)) {
|
||||
return false;
|
||||
}
|
||||
$this->user->ordinal()
|
||||
->set(
|
||||
'personal-collage',
|
||||
(int)self::$db->scalar("
|
||||
SELECT collage_total FROM users_main WHERE ID = ?
|
||||
", $this->user->id
|
||||
)
|
||||
);
|
||||
$this->addPurchaseHistory($item['ID'], $price);
|
||||
$this->flush();
|
||||
return true;
|
||||
}
|
||||
|
||||
public function unlockSeedbox(): bool {
|
||||
$item = $this->items()['seedbox'];
|
||||
$price = $this->effectivePrice('seedbox');
|
||||
self::$db->begin_transaction();
|
||||
self::$db->prepared_query('
|
||||
UPDATE user_bonus ub SET
|
||||
ub.points = ub.points - ?
|
||||
WHERE ub.points >= ?
|
||||
AND ub.user_id = ?
|
||||
', $price, $price, $this->user->id
|
||||
);
|
||||
if (self::$db->affected_rows() != 1) {
|
||||
self::$db->rollback();
|
||||
return false;
|
||||
}
|
||||
try {
|
||||
self::$db->prepared_query("
|
||||
INSERT INTO user_has_attr
|
||||
(UserID, UserAttrID)
|
||||
VALUES (?, (SELECT ID FROM user_attr WHERE Name = ?))
|
||||
", $this->user->id, 'feature-seedbox'
|
||||
);
|
||||
} catch (\Gazelle\DB\MysqlDuplicateKeyException) {
|
||||
// no point in buying a second time
|
||||
self::$db->rollback();
|
||||
return false;
|
||||
}
|
||||
self::$db->commit();
|
||||
$this->addPurchaseHistory($item['ID'], $price);
|
||||
$this->flush();
|
||||
return true;
|
||||
}
|
||||
|
||||
public function purchaseFeatureFilecount(): bool {
|
||||
$item = $this->items()['file-count'];
|
||||
$price = $this->effectivePrice('file-count');
|
||||
self::$db->begin_transaction();
|
||||
self::$db->prepared_query('
|
||||
UPDATE user_bonus ub SET
|
||||
ub.points = ub.points - ?
|
||||
WHERE ub.points >= ?
|
||||
AND ub.user_id = ?
|
||||
', $price, $price, $this->user->id
|
||||
);
|
||||
if (self::$db->affected_rows() != 1) {
|
||||
self::$db->rollback();
|
||||
return false;
|
||||
}
|
||||
try {
|
||||
self::$db->prepared_query("
|
||||
INSERT INTO user_has_attr
|
||||
(UserID, UserAttrID)
|
||||
VALUES (?, (SELECT ID FROM user_attr WHERE Name = ?))
|
||||
", $this->user->id, 'feature-file-count'
|
||||
);
|
||||
} catch (\Gazelle\DB\MysqlDuplicateKeyException) {
|
||||
// no point in buying a second time
|
||||
self::$db->rollback();
|
||||
return false;
|
||||
}
|
||||
self::$db->commit();
|
||||
$this->addPurchaseHistory($item['ID'], $price);
|
||||
$this->flush();
|
||||
return true;
|
||||
}
|
||||
|
||||
public function purchaseToken(string $label): bool {
|
||||
$item = $this->items()[$label];
|
||||
if (!$item) {
|
||||
return false;
|
||||
}
|
||||
$amount = (int)$item['Amount'];
|
||||
$price = $item['Price'];
|
||||
self::$db->prepared_query('
|
||||
UPDATE user_bonus ub
|
||||
INNER JOIN user_flt uf USING (user_id) SET
|
||||
ub.points = ub.points - ?,
|
||||
uf.tokens = uf.tokens + ?
|
||||
WHERE ub.user_id = ?
|
||||
AND ub.points >= ?
|
||||
', $price, $amount, $this->user->id, $price
|
||||
);
|
||||
if (self::$db->affected_rows() != 2) {
|
||||
return false;
|
||||
}
|
||||
$this->addPurchaseHistory($item['ID'], $price);
|
||||
$this->flush();
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* This method does not return a boolean success, but rather the number of
|
||||
* tokens purchased (for use in a response to the receiver).
|
||||
*/
|
||||
public function purchaseTokenOther(\Gazelle\User $receiver, string $label, string $message): int {
|
||||
if ($this->user->id === $receiver->id) {
|
||||
return 0;
|
||||
}
|
||||
$item = $this->items()[$label];
|
||||
if (!$item) {
|
||||
return 0;
|
||||
}
|
||||
$amount = (int)$item['Amount'];
|
||||
$price = $item['Price'];
|
||||
|
||||
/* Take the bonus points from the giver and give tokens
|
||||
* to the receiver, unless the latter have asked to
|
||||
* refuse receiving tokens.
|
||||
*/
|
||||
self::$db->prepared_query("
|
||||
UPDATE user_bonus ub
|
||||
INNER JOIN users_main self ON (self.ID = ub.user_id),
|
||||
users_main other
|
||||
INNER JOIN user_flt other_uf ON (other_uf.user_id = other.ID)
|
||||
LEFT JOIN user_has_attr noFL ON (noFL.UserID = other.ID AND noFL.UserAttrId
|
||||
= (SELECT ua.ID FROM user_attr ua WHERE ua.Name = 'no-fl-gifts')
|
||||
)
|
||||
SET
|
||||
ub.points = ub.points - ?,
|
||||
other_uf.tokens = other_uf.tokens + ?
|
||||
WHERE noFL.UserID IS NULL
|
||||
AND other.Enabled = '1'
|
||||
AND other.ID = ?
|
||||
AND self.ID = ?
|
||||
AND ub.points >= ?
|
||||
", $price, $amount, $receiver->id, $this->user->id, $price
|
||||
);
|
||||
if (self::$db->affected_rows() != 2) {
|
||||
return 0;
|
||||
}
|
||||
$this->addPurchaseHistory($item['ID'], $price, $receiver->id);
|
||||
$this->sendPmToOther($receiver, $amount, $message);
|
||||
$this->flush();
|
||||
$receiver->flush();
|
||||
|
||||
return $amount;
|
||||
}
|
||||
|
||||
public function sendPmToOther(\Gazelle\User $receiver, int $amount, string $message): \Gazelle\PM {
|
||||
return $receiver->inbox()->createSystem(
|
||||
"Here " . ($amount == 1 ? 'is' : 'are') . ' ' . article($amount) . " freeleech token" . plural($amount) . "!",
|
||||
self::$twig->render('bonus/token-other-message.bbcode.twig', [
|
||||
'to' => $receiver->username(),
|
||||
'from' => $this->user->username(),
|
||||
'amount' => $amount,
|
||||
'wiki_id' => 57,
|
||||
'message' => $message
|
||||
])
|
||||
);
|
||||
}
|
||||
|
||||
private function addPurchaseHistory(int $itemId, int $price, int|null $otherUserId = null): int {
|
||||
self::$cache->delete_multi([
|
||||
sprintf(self::CACHE_PURCHASE, $this->user->id),
|
||||
sprintf(self::CACHE_SUMMARY, $this->user->id),
|
||||
sprintf(self::CACHE_HISTORY, $this->user->id, 0)
|
||||
]);
|
||||
self::$db->prepared_query("
|
||||
INSERT INTO bonus_history
|
||||
(ItemID, UserID, Price, OtherUserID)
|
||||
VALUES (?, ?, ?, ?)
|
||||
", $itemId, $this->user->id, $price, $otherUserId
|
||||
);
|
||||
return self::$db->affected_rows();
|
||||
}
|
||||
|
||||
public function setPoints(float $points): int {
|
||||
self::$db->prepared_query("
|
||||
UPDATE user_bonus SET
|
||||
|
||||
@@ -183,7 +183,7 @@ $tgMan = new Manager\TGroup();
|
||||
foreach ($groupList as $tgroupId) {
|
||||
$tgroup = $tgMan->findById($tgroupId);
|
||||
if ($tgroup instanceof TGroup) {
|
||||
echo "tgroup $torrentId ({$tgroup->name()})\n";
|
||||
echo "tgroup $torrentId ({$tgroup?->name()})\n";
|
||||
$db->prepared_query("
|
||||
DELETE FROM bookmarks_torrents bt WHERE GroupID = ?
|
||||
", $tgroupId
|
||||
|
||||
81
misc/pg-migrations/20250824000000_bonus_item.php
Normal file
81
misc/pg-migrations/20250824000000_bonus_item.php
Normal file
@@ -0,0 +1,81 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
use Phinx\Migration\AbstractMigration;
|
||||
use Phinx\Util\Literal;
|
||||
|
||||
final class BonusItem extends AbstractMigration {
|
||||
public function up(): void {
|
||||
// An earlier migration created this table as a mirror of the Mysql
|
||||
// table, but no code was ever written to make use of it. Therefore
|
||||
// delete if it exists. If this migration is rolled back, it does
|
||||
// not bother to recreate the table, which means that when it is
|
||||
// migrated a second time, the table will no longer be present.
|
||||
// Hence "if exists".
|
||||
$this->execute('drop table if exists bonus_item');
|
||||
|
||||
$this->query("create type bonus_item_freq_t as enum ('one-time', 'recurring')");
|
||||
$this->table('bonus_item', ['id' => false, 'primary_key' => 'id_bonus_item'])
|
||||
->addColumn('id_bonus_item', 'integer', ['identity' => true])
|
||||
->addColumn('sequence', 'integer')
|
||||
->addColumn('price', 'integer')
|
||||
->addColumn('amount', 'integer')
|
||||
->addColumn('userclass_min', 'integer')
|
||||
->addColumn('userclass_free', 'integer')
|
||||
->addColumn('frequency', Literal::from('bonus_item_freq_t'))
|
||||
->addColumn('prepare', 'boolean')
|
||||
->addColumn('label', 'string', ['length' => 32])
|
||||
->addColumn('title', 'string', ['length' => 64])
|
||||
->save();
|
||||
|
||||
$this->execute('create unique index bi_l_uidx on bonus_item (label);');
|
||||
$this->execute('create unique index bi_s_uidx on bonus_item (sequence);');
|
||||
|
||||
$this->query(<<<END_SQL
|
||||
insert into bonus_item (
|
||||
id_bonus_item,
|
||||
sequence,
|
||||
price,
|
||||
amount,
|
||||
userclass_min,
|
||||
userclass_free,
|
||||
frequency,
|
||||
label,
|
||||
title,
|
||||
prepare
|
||||
)
|
||||
select "ID",
|
||||
sequence,
|
||||
"Price",
|
||||
coalesce("Amount", 0),
|
||||
coalesce("MinClass", 1),
|
||||
"FreeClass",
|
||||
cast(
|
||||
case when "Label" in ('seedbox', 'file-count')
|
||||
then 'one-time'
|
||||
else 'recurring'
|
||||
end
|
||||
as bonus_item_freq_t
|
||||
),
|
||||
"Label",
|
||||
case
|
||||
when "Title" = 'Buy an Invite' then 'Personal Invite'
|
||||
when "Title" = 'Buy a Personal Collage Slot' then 'Personal Collage'
|
||||
when "Title" = 'Unlock the Seedbox viewer' then 'Seedbox Viewer'
|
||||
else "Title"
|
||||
end,
|
||||
case when "Label" ~ '^(other-|title-bb-[yn])'
|
||||
then true
|
||||
else false
|
||||
end
|
||||
from relay.bonus_item
|
||||
END_SQL
|
||||
);
|
||||
}
|
||||
|
||||
public function down(): void {
|
||||
$this->table('bonus_item')->drop()->save();
|
||||
$this->query('drop type bonus_item_freq_t');
|
||||
}
|
||||
}
|
||||
@@ -1,34 +1,71 @@
|
||||
function PreviewTitle(BBCode) {
|
||||
$.post('bonus.php?action=title&preview=true', {
|
||||
title: $('#title').val(),
|
||||
BBCode: BBCode
|
||||
}, function(response) {
|
||||
$('#preview').html(response);
|
||||
});
|
||||
"use strict";
|
||||
|
||||
async function previewTitle() {
|
||||
const form = new FormData();
|
||||
form.append('auth', document.body.dataset.auth);
|
||||
form.append('title', document.getElementById('title').value);
|
||||
form.append('label', document.forms['custom-title-preview'].elements['label'].value);
|
||||
const response = await fetch(
|
||||
'?action=prepare', {
|
||||
'method': 'POST',
|
||||
'body': form,
|
||||
}
|
||||
);
|
||||
const data = await response.json();
|
||||
document.getElementById('preview').innerHTML =
|
||||
data.status === 'success'
|
||||
? data.response[0]
|
||||
: data.status;
|
||||
}
|
||||
|
||||
function NoOp(event, item, next, element) {
|
||||
return next && next(event, element);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {Object} event
|
||||
* @param {String} item
|
||||
* @param {Function} next
|
||||
* @param {Object} element
|
||||
* @return {boolean}
|
||||
*/
|
||||
function ConfirmPurchase(event, item, next, element) {
|
||||
var check = (next) ? next(event, element) : true;
|
||||
if (!check) {
|
||||
event.preventDefault();
|
||||
return false;
|
||||
async function validateBonusUsername() {
|
||||
const status = document.getElementById('bonus-other-status');
|
||||
const username = document.getElementById('bonus-user-other').value.trim();
|
||||
const purchase = document.getElementById('bonus-other-purchase')
|
||||
purchase.disabled = true;
|
||||
if (username === '') {
|
||||
status.innerHTML = '';
|
||||
return;
|
||||
}
|
||||
check = confirm('Are you sure you want to purchase ' + item + '?');
|
||||
if (!check) {
|
||||
event.preventDefault();
|
||||
return false;
|
||||
const form = new FormData();
|
||||
form.append('auth', document.body.dataset.auth);
|
||||
form.append('bonus-user-other', username);
|
||||
const response = await fetch(
|
||||
'?action=prepare', {
|
||||
'method': 'POST',
|
||||
'body': form,
|
||||
}
|
||||
);
|
||||
const data = await response.json();
|
||||
let message;
|
||||
if (data.status !== 'success') {
|
||||
message = status;
|
||||
} else {
|
||||
const user = data.response;
|
||||
if (!user.found) {
|
||||
message = '⛔️ ' + 'user not found';
|
||||
} else if (user.id == document.body.dataset.id) {
|
||||
message = '⛔️ You cannot gift tokens to yourself';
|
||||
} else if (!user.enabled) {
|
||||
message = '⛔️ ' + user.username + ' is currently not enabled';
|
||||
} else if (!user.accept) {
|
||||
message = '🚫 ' + user.username + ' does not wish to receive tokens';
|
||||
} else {
|
||||
message = '✅';
|
||||
document.forms['bonus-other'].elements['user'].value = user.username;
|
||||
purchase.disabled = false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
|
||||
status.innerHTML = message;
|
||||
}
|
||||
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
const button = document.getElementById('preview-title');
|
||||
if (button) {
|
||||
button.addEventListener('click', () => previewTitle());
|
||||
}
|
||||
const username = document.getElementById('bonus-user-other');
|
||||
if (username) {
|
||||
username.addEventListener('change', () => validateBonusUsername());
|
||||
}
|
||||
});
|
||||
|
||||
@@ -9,48 +9,25 @@ if ($Viewer->disableBonusPoints()) {
|
||||
Error403::error('Your bonus points have been deactivated.');
|
||||
}
|
||||
|
||||
const DEFAULT_PAGE = 'store.php';
|
||||
|
||||
switch ($_GET['action'] ?? '') {
|
||||
case 'purchase':
|
||||
/* handle validity and cost as early as possible */
|
||||
if (preg_match('/^[a-z]{1,15}(-\w{1,15}){0,4}/', $_REQUEST['label'] ?? '')) {
|
||||
$viewerBonus = new \Gazelle\User\Bonus($Viewer);
|
||||
$Label = $_REQUEST['label'];
|
||||
$Item = $viewerBonus->item($Label);
|
||||
if (!$Item) {
|
||||
include_once DEFAULT_PAGE;
|
||||
break;
|
||||
}
|
||||
$Price = $viewerBonus->effectivePrice($Label);
|
||||
if ($Price > $Viewer->bonusPointsTotal()) {
|
||||
Error400::error('You cannot afford this item.');
|
||||
}
|
||||
include_once match ($Label) {
|
||||
'invite' => 'invite.php',
|
||||
'collage-1', 'seedbox', 'file-count' => 'purchase.php',
|
||||
'title-bb-y', 'title-bb-n', 'title-off' => 'title.php',
|
||||
'token-1', 'token-2', 'token-3', 'token-4' => 'tokens.php',
|
||||
'other-1', 'other-2', 'other-3', 'other-4' => 'token_other.php',
|
||||
default => DEFAULT_PAGE,
|
||||
};
|
||||
}
|
||||
break;
|
||||
switch ($_REQUEST['action'] ?? '') {
|
||||
case 'bprates':
|
||||
include_once 'bprates.php';
|
||||
break;
|
||||
case 'title':
|
||||
include_once 'title.php';
|
||||
break;
|
||||
case 'cacheflush':
|
||||
new Manager\Bonus()->flush();
|
||||
header("Location: bonus.php");
|
||||
exit;
|
||||
case 'history':
|
||||
include_once 'history.php';
|
||||
break;
|
||||
case 'cacheflush':
|
||||
new \Gazelle\Manager\Bonus()->flushPriceCache();
|
||||
header("Location: bonus.php");
|
||||
exit;
|
||||
case 'prepare':
|
||||
include_once 'prepare.php';
|
||||
break;
|
||||
case 'purchase':
|
||||
include_once 'purchase.php';
|
||||
break;
|
||||
case 'donate':
|
||||
default:
|
||||
include_once DEFAULT_PAGE;
|
||||
include_once 'shop.php';
|
||||
break;
|
||||
}
|
||||
|
||||
@@ -1,14 +0,0 @@
|
||||
<?php
|
||||
/** @phpstan-var \Gazelle\User $Viewer */
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Gazelle;
|
||||
|
||||
authorize();
|
||||
if (!new User\Bonus($Viewer)->purchaseInvite()) {
|
||||
Error400::error(
|
||||
"You cannot purchase an invite (either you don't have the privilege or you don't have enough bonus points)."
|
||||
);
|
||||
}
|
||||
header('Location: bonus.php?complete=invite');
|
||||
39
sections/bonus/prepare.php
Normal file
39
sections/bonus/prepare.php
Normal file
@@ -0,0 +1,39 @@
|
||||
<?php
|
||||
/** @phpstan-var \Gazelle\User $Viewer */
|
||||
/** @phpstan-var \Twig\Environment $Twig */
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Gazelle;
|
||||
|
||||
if (isset($_POST['label'], $_POST['title'])) {
|
||||
authorize(ajax: true);
|
||||
echo new Json\BonusItemTitle($_POST['label'], $_POST['title'])->response();
|
||||
exit;
|
||||
}
|
||||
|
||||
if (isset($_POST['bonus-user-other'])) {
|
||||
authorize(ajax: true);
|
||||
echo new Json\BonusUserOther($_POST['bonus-user-other'])->response();
|
||||
exit;
|
||||
}
|
||||
|
||||
$item = new Manager\Bonus()->findBonusItemByLabel($_REQUEST['item'] ?? '');
|
||||
if (is_null($item)) {
|
||||
Error400::error('Unknown bonus shop item');
|
||||
}
|
||||
|
||||
if (str_starts_with($item->label(), 'title-bb-')) {
|
||||
echo $Twig->render('bonus/title.twig', [
|
||||
'item' => $item,
|
||||
'viewer' => $Viewer,
|
||||
]);
|
||||
} elseif (str_starts_with($item->label(), 'other-')) {
|
||||
echo $Twig->render('bonus/token-other.twig', [
|
||||
'item' => $item,
|
||||
'message' => new Util\Textarea('message', ''),
|
||||
'viewer' => $Viewer,
|
||||
]);
|
||||
} else {
|
||||
Error400::error("Buying this item requires no preparation");
|
||||
}
|
||||
@@ -7,24 +7,54 @@ namespace Gazelle;
|
||||
|
||||
authorize();
|
||||
|
||||
$label = $_REQUEST['label'];
|
||||
$bonus = new User\Bonus($Viewer);
|
||||
|
||||
if ($label === 'collage-1') {
|
||||
if (!$bonus->purchaseCollage($label)) {
|
||||
Error400::error('Could not purchase a personal collage slot due to lack of funds.');
|
||||
}
|
||||
header("Location: bonus.php?complete=$label");
|
||||
} elseif ($label === 'seedbox') {
|
||||
if (!$bonus->unlockSeedbox()) {
|
||||
Error400::error('Could not unlock the seedbox viewer. Either you have already unlocked it, or you lack the required bonus points.');
|
||||
}
|
||||
header("Location: bonus.php?complete=$label");
|
||||
} elseif ($label === 'file-count') {
|
||||
if (!$bonus->purchaseFeatureFilecount()) {
|
||||
Error400::error('Could not purchase the file count feature. Either you have already own it, or you lack the required bonus points.');
|
||||
}
|
||||
header("Location: bonus.php?complete=$label");
|
||||
} else {
|
||||
Error403::error();
|
||||
$label = $_POST['label'] // from a prepare page
|
||||
?? current(
|
||||
array_keys(
|
||||
array_filter(
|
||||
$_POST,
|
||||
fn ($i) => $i === 'Purchase!' // from the shop page
|
||||
)
|
||||
)
|
||||
);
|
||||
if (!is_string($label)) {
|
||||
Error400::error("Not sure what you were planning to purchase");
|
||||
}
|
||||
$item = new Manager\Bonus()->findBonusItemByLabel($label);
|
||||
if (is_null($item)) {
|
||||
Error404::error("We don't stock that item");
|
||||
}
|
||||
|
||||
$extra = [];
|
||||
if (str_starts_with($item->label(), 'title-bb-')) {
|
||||
$extra['title'] = $_POST['title'] ?? '';
|
||||
} elseif (str_starts_with($item->label(), 'other-')) {
|
||||
$user = new Manager\User()->findByUsername($_POST['user'] ?? '');
|
||||
if (is_null($user)) {
|
||||
Error404::error(
|
||||
'Nobody with that name found. Try a user search and give them tokens from their profile page.'
|
||||
);
|
||||
} elseif ($user->id == $Viewer->id) {
|
||||
Error400::error('You cannot gift yourself tokens, they are cheaper to buy directly.');
|
||||
}
|
||||
$extra['receiver'] = $user;
|
||||
$extra['message'] = $_POST['message'] ?? '';
|
||||
}
|
||||
|
||||
// phpcs:disable PSR2.ControlStructures.SwitchDeclaration.TerminatingComment
|
||||
switch ($item->purchase($Viewer, $item->price(), $extra)) {
|
||||
case Enum\BonusItemPurchaseStatus::success:
|
||||
header("Location: bonus.php?complete={$item->label()}");
|
||||
exit;
|
||||
|
||||
case Enum\BonusItemPurchaseStatus::insufficientFunds:
|
||||
Error400::error("Could not purchase {$item->title()} due to lack of funds.");
|
||||
|
||||
case Enum\BonusItemPurchaseStatus::forbidden:
|
||||
Error400::error("You are not allowed to purchase {$item->title()}.");
|
||||
|
||||
case Enum\BonusItemPurchaseStatus::alreadyPurchased:
|
||||
Error400::error("You already own {$item->title()}.");
|
||||
|
||||
case Enum\BonusItemPurchaseStatus::incomplete:
|
||||
Error400::error("Information lacking to complete purchase of {$item->title()}.");
|
||||
}
|
||||
|
||||
@@ -9,7 +9,7 @@ namespace Gazelle;
|
||||
$bonus = new User\Bonus($Viewer);
|
||||
$bonusMan = new Manager\Bonus();
|
||||
|
||||
$purchase = isset($_GET['complete']) ? $bonus->item($_GET['complete'])['Title'] : false;
|
||||
$purchase = $bonusMan->findBonusItemByLabel($_GET['complete'] ?? '');
|
||||
if (($_GET['action'] ?? '') !== 'donate') {
|
||||
$donate = false;
|
||||
} else {
|
||||
@@ -34,10 +34,11 @@ if (($_GET['action'] ?? '') !== 'donate') {
|
||||
}
|
||||
}
|
||||
|
||||
echo $Twig->render('bonus/store.twig', [
|
||||
echo $Twig->render('bonus/shop.twig', [
|
||||
'bonus' => $bonus,
|
||||
'discount' => $bonusMan->discount(),
|
||||
'donate' => $donate,
|
||||
'list' => $bonusMan->itemList(),
|
||||
'pool' => $bonusMan->openPoolList(),
|
||||
'purchase' => $purchase,
|
||||
'viewer' => $Viewer,
|
||||
@@ -6,52 +6,12 @@ declare(strict_types=1);
|
||||
|
||||
namespace Gazelle;
|
||||
|
||||
/**
|
||||
* @var array $Item
|
||||
* @var int $Price
|
||||
*/
|
||||
|
||||
if (isset($_REQUEST['preview']) && isset($_REQUEST['title']) && isset($_REQUEST['BBCode'])) {
|
||||
echo $_REQUEST['BBCode'] === 'true'
|
||||
? \Text::full_format($_REQUEST['title'])
|
||||
: \Text::strip_bbcode($_REQUEST['title']);
|
||||
exit;
|
||||
}
|
||||
|
||||
$Label = $_REQUEST['label'];
|
||||
if ($Label === 'title-off') {
|
||||
authorize();
|
||||
$Viewer->removeTitle()->modify();
|
||||
header('Location: bonus.php?complete=' . urlencode($Label));
|
||||
exit;
|
||||
}
|
||||
if ($Label === 'title-bb-y') {
|
||||
$BBCode = 'true';
|
||||
} elseif ($Label === 'title-bb-n') {
|
||||
$BBCode = 'false';
|
||||
} else {
|
||||
Error403::error();
|
||||
}
|
||||
|
||||
if (isset($_POST['confirm'])) {
|
||||
authorize();
|
||||
if (!isset($_POST['title'])) {
|
||||
Error403::error();
|
||||
}
|
||||
$viewerBonus = new \Gazelle\User\Bonus($Viewer);
|
||||
if (!$viewerBonus->purchaseTitle($Label, $_POST['title'])) {
|
||||
Error400::error(
|
||||
'This title is too long, you must reduce the length (or you do not have enough bonus points).'
|
||||
);
|
||||
}
|
||||
header('Location: bonus.php?complete=' . urlencode($Label));
|
||||
exit;
|
||||
$item = new Manager\Bonus()->findBonusItemByLabel($_REQUEST['label'] ?? '');
|
||||
if (is_null($item)) {
|
||||
Error400::error('Unknown bonus shop item');
|
||||
}
|
||||
|
||||
echo $Twig->render('bonus/title.twig', [
|
||||
'bbcode' => $BBCode,
|
||||
'label' => $Label,
|
||||
'price' => $Price,
|
||||
'title' => $Item['Title'],
|
||||
'item' => $item,
|
||||
'viewer' => $Viewer,
|
||||
]);
|
||||
|
||||
@@ -6,36 +6,15 @@ declare(strict_types=1);
|
||||
|
||||
namespace Gazelle;
|
||||
|
||||
/**
|
||||
* @var array $Item
|
||||
* @var string $Label
|
||||
* @var int $Price
|
||||
*/
|
||||
authorize();
|
||||
|
||||
if (isset($_POST['confirm'])) {
|
||||
authorize();
|
||||
if (empty($_POST['user'])) {
|
||||
Error404::error('You have to enter a username to give tokens to.');
|
||||
}
|
||||
$user = new Manager\User()->findByUsername(urldecode($_POST['user']));
|
||||
if (is_null($user)) {
|
||||
Error404::error(
|
||||
'Nobody with that name found. Try a user search and give them tokens from their profile page.'
|
||||
);
|
||||
} elseif ($user->id == $Viewer->id()) {
|
||||
Error400::error('You cannot gift yourself tokens, they are cheaper to buy directly.');
|
||||
}
|
||||
$viewerBonus = new \Gazelle\User\Bonus($Viewer);
|
||||
if (!$viewerBonus->purchaseTokenOther($user, $Label, $_POST['message'] ?? '')) {
|
||||
Error400::error('Purchase for other not concluded. Either you lacked funds or they have chosen to decline FL tokens.');
|
||||
}
|
||||
header('Location: bonus.php?complete=' . urlencode($Label));
|
||||
$item = new Manager\Bonus()->findBonusItemByLabel($_REQUEST['label'] ?? '');
|
||||
if (is_null($item)) {
|
||||
Error400::error('Unknown bonus shop item');
|
||||
}
|
||||
|
||||
echo $Twig->render('bonus/token-other.twig', [
|
||||
'auth' => $Viewer->auth(),
|
||||
'price' => $Price,
|
||||
'label' => $Label,
|
||||
'textarea' => new Util\Textarea('message', ''),
|
||||
'item' => $Item['Title']
|
||||
'item' => $item,
|
||||
'message' => new Util\Textarea('message', ''),
|
||||
'viewer' => $Viewer,
|
||||
]);
|
||||
|
||||
@@ -1,25 +0,0 @@
|
||||
<?php
|
||||
/** @phpstan-var \Gazelle\User $Viewer */
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Gazelle;
|
||||
|
||||
/**
|
||||
* @var string $Label
|
||||
*/
|
||||
|
||||
authorize();
|
||||
|
||||
if (!preg_match('/^token-[1-4]$/', $Label, $match)) {
|
||||
Error403::error();
|
||||
}
|
||||
|
||||
$viewerBonus = new \Gazelle\User\Bonus($Viewer);
|
||||
if (!$viewerBonus->purchaseToken($Label)) {
|
||||
Error400::error(
|
||||
"You aren't able to buy those tokens. Do you have enough bonus points?"
|
||||
);
|
||||
}
|
||||
|
||||
header('Location: bonus.php?complete=' . urlencode($Label));
|
||||
@@ -10,6 +10,7 @@ declare(strict_types=1);
|
||||
|
||||
namespace Gazelle;
|
||||
|
||||
use Gazelle\Enum\BonusItemPurchaseStatus;
|
||||
use Gazelle\Enum\UserTokenType;
|
||||
use Gazelle\User\Vote;
|
||||
|
||||
@@ -28,34 +29,44 @@ $userBonus = new User\Bonus($user);
|
||||
$viewerBonus = new User\Bonus($Viewer);
|
||||
$history = new User\History($user);
|
||||
$limiter = new User\UserclassRateLimit($user);
|
||||
$bonusMan = new Manager\Bonus();
|
||||
$donorMan = new Manager\Donation();
|
||||
$ipv4 = new Manager\IPv4();
|
||||
$tgMan = new Manager\TGroup();
|
||||
|
||||
$resetToken = $Viewer->permitted('users_mod')
|
||||
? new Manager\UserToken()->findByUser($user, UserTokenType::password)
|
||||
: null;
|
||||
if (!empty($_POST)) {
|
||||
|
||||
if (isset($_POST['flsubmit'], $_POST['fltype']) && ($_POST['action'] ?? '') === 'fltoken') {
|
||||
authorize();
|
||||
foreach (['action', 'flsubmit', 'fltype'] as $arg) {
|
||||
if (!isset($_POST[$arg])) {
|
||||
Error403::error();
|
||||
}
|
||||
if (str_starts_with($_POST['fltype'], 'fl-')) {
|
||||
// backwards compatibility
|
||||
$_POST['fltype'] = substr($_POST['fltype'], 3);
|
||||
}
|
||||
if ($_POST['action'] !== 'fltoken' || $_POST['flsubmit'] !== 'Send') {
|
||||
Error403::error();
|
||||
}
|
||||
if (!preg_match('/^fl-(other-[1-4])$/', $_POST['fltype'], $match)) {
|
||||
Error403::error();
|
||||
}
|
||||
$FL_OTHER_tokens = $viewerBonus->purchaseTokenOther($user, $match[1], $_POST['message'] ?? '');
|
||||
if (!$FL_OTHER_tokens) {
|
||||
$status = $bonusMan->purchaseTokenOther(
|
||||
$Viewer, $user, $_POST['fltype'], $_POST['message'] ?? ''
|
||||
);
|
||||
if ($status === BonusItemPurchaseStatus::success) {
|
||||
header("Location: {$user->location()}");
|
||||
exit;
|
||||
} else {
|
||||
Error400::error(
|
||||
'Purchase of tokens not concluded. Either you lacked funds or they have chosen to decline FL tokens.'
|
||||
match ($status) { /* @phpstan-ignore match.unhandled (alreadyPurchased) */
|
||||
BonusItemPurchaseStatus::declined
|
||||
=> "{$user->username()} does not wish to receive tokens",
|
||||
BonusItemPurchaseStatus::insufficientFunds
|
||||
=> "You do not have enough bonus points to buy that",
|
||||
BonusItemPurchaseStatus::forbidden
|
||||
=> "{$user->username()} cannot receive tokens from you",
|
||||
BonusItemPurchaseStatus::incomplete
|
||||
=> "Something was wrong with the parameters provided",
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
if ($userId == $Viewer->id()) {
|
||||
if ($userId == $Viewer->id) {
|
||||
$Preview = (bool)($_GET['preview'] ?? false);
|
||||
$OwnProfile = !$Preview;
|
||||
$user->forceCacheFlush(true);
|
||||
@@ -90,8 +101,7 @@ echo $Twig->render('user/header.twig', [
|
||||
'bonus' => $userBonus,
|
||||
'donor' => $donor,
|
||||
'freeleech' => [
|
||||
'item' => $OwnProfile ? [] : $viewerBonus->otherList(),
|
||||
'other' => $FL_OTHER_tokens ?? null,
|
||||
'offer' => $OwnProfile ? [] : $bonusMan->offerTokenOther($Viewer),
|
||||
'latest' => $viewerBonus->otherLatest($user),
|
||||
],
|
||||
'friend' => new User\Friend($Viewer),
|
||||
|
||||
@@ -11,10 +11,10 @@
|
||||
{% endif %}
|
||||
</div>
|
||||
{% if purchase %}
|
||||
<div class="alertbar blend">{{ purchase }} purchased!</div>
|
||||
<div class="save_message">✅ {{ purchase.title }} purchased!</div>
|
||||
{% endif %}
|
||||
{% if donate %}
|
||||
<div class="alertbar blend">{{ donate }}</div>
|
||||
<div class="save_message">{{ donate }}</div>
|
||||
{% endif %}
|
||||
{% if pool %}
|
||||
{% include 'bonus/bonus-pool.twig' with {
|
||||
@@ -23,48 +23,58 @@
|
||||
} only
|
||||
%}
|
||||
{% endif %}
|
||||
{% for item in bonus.itemList %}
|
||||
{% for item in list %}
|
||||
{% if loop.first %}
|
||||
<div class="thin">
|
||||
{% if discount %}
|
||||
<h3 style="text-align: center; color: lime;">All prices currently {{ min(100, max(0, discount))
|
||||
}}% off — Hurry, sale ends soon — While stocks last!</h3>
|
||||
}}% off — Hurry, sale ends soon — While stocks last!</h3>
|
||||
{% endif %}
|
||||
{% if viewer.permitted('admin_bp_history') %}
|
||||
<div class="pad box">
|
||||
<div class="thin">NB: Bonus Shop discounts are set in the <a href="/tools.php?action=site_options">Site Options</a>.</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
<form action="" method="post">
|
||||
<input type="hidden" name="auth" value="{{ viewer.auth }}" />
|
||||
<input type="hidden" name="action" value="purchase" />
|
||||
<table>
|
||||
<thead>
|
||||
<tr class="colhead">
|
||||
<td>Description</td>
|
||||
<td style="width:60px">Points</td>
|
||||
<td style="width:120px">Checkout</td>
|
||||
<td class="number_column">Points</td>
|
||||
<td style="text-align:center">Checkout</td>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% endif %}
|
||||
{% if item.MinClass <= viewer.classLevel %}
|
||||
<tr class="row{{ cycle(['a', 'b'], loop.index0) }}">
|
||||
<td>{{ item.Title }}</pre></td>
|
||||
<td style="text-align:right">{{ item.Price|number_format }}</td>
|
||||
<td>
|
||||
{% if viewer.bonusPointsTotal >= item.Price %}
|
||||
<a id="bonusconfirm" href="bonus.php?action=purchase&label={{ item.Label
|
||||
}}&auth={{ viewer.auth }}" onclick="{{ item.JS_on_clic
|
||||
}}(event, '{{ item.Title }}', {{ item.JS_next_function|default('null')
|
||||
}}, this);">Purchase</a>
|
||||
{% else %}
|
||||
<span style="font-style: italic">Too Expensive</span>
|
||||
|
||||
{% endif %}
|
||||
<td>{{ item.title }}</td>
|
||||
<td style="text-align:right">{{ item.priceForUser(viewer)|number_format }}</td>
|
||||
<td style="text-align:center">
|
||||
{% if not item.permitted(viewer) %}
|
||||
<i>Forbidden</i>
|
||||
{% elseif item.userclassMin > viewer.classLevel %}
|
||||
<i>Locked</i>
|
||||
{% elseif
|
||||
(item.label == 'seedbox' and viewer.hasAttr('feature-seedbox'))
|
||||
or
|
||||
(item.label == 'file-count' and viewer.hasAttr('feature-file-count'))
|
||||
%}
|
||||
<i>You own this</i>
|
||||
{% elseif item.priceForUser(viewer) > viewer.bonusPointsTotal %}
|
||||
<i>Too Expensive</i>
|
||||
{% elseif item.needsPreparation %}
|
||||
<a href="bonus.php?action=prepare&item={{ item.label }}">Prepare</a>
|
||||
{% else %}
|
||||
<input style="font-size:10px;margin:0px;" type="submit" name="{{ item.label }}" value="Purchase!" />
|
||||
{% endif %}
|
||||
</td>
|
||||
</tr>
|
||||
</tr>
|
||||
{% if loop.last %}
|
||||
</tbody>
|
||||
</table>
|
||||
</form>
|
||||
<br />
|
||||
</div>
|
||||
{% endif %}
|
||||
@@ -3,17 +3,23 @@
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<td>Custom Title, {% if not bbcode %}no {% endif %} allowed - {{ price|number_format }} Points</td>
|
||||
<td>
|
||||
<a href="bonus.php">Shop</a> › Custom Title, BBCode {% if item.label != 'title-bb-y' %}not {% endif
|
||||
%} allowed - {{ item.priceForUser(viewer)|number_format }} Points</td>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td>
|
||||
<form action="bonus.php?action=purchase&label={{ label }}" method="post">
|
||||
<form name="custom-title-preview" action="bonus.php" method="post">
|
||||
<input type="hidden" name="action" value="purchase" />
|
||||
<input type="hidden" name="auth" value="{{ viewer.auth }}" />
|
||||
<input type="hidden" name="confirm" value="true" />
|
||||
<input type="hidden" name="label" value="{{ item.label }}" />
|
||||
<input type="text" style="width: 98%" id="title" name="title" placeholder="Custom Title"/> <br />
|
||||
<input type="submit" onclick="ConfirmPurchase(event, '{{ title }}')" value="Submit" /> <input type="button" onclick="PreviewTitle({{ bbcode }});" value="Preview" /><br /><br />
|
||||
<br />
|
||||
<input type="button" id="preview-title" value="Preview" />
|
||||
<input type="submit" value="Purchase" />
|
||||
<br /><br />
|
||||
<div id="preview"></div>
|
||||
</form>
|
||||
</td>
|
||||
|
||||
@@ -1,37 +1,39 @@
|
||||
{{ header("Bonus Points - Gift Tokens ", {'js': 'bonus'}) }}
|
||||
<div class="thin">
|
||||
<div class="header">
|
||||
<h2>Gift Tokens - {{price | number_format}} Points</a></h2>
|
||||
<h2><a href="bonus.php">Shop</a> › Gift Tokens - {{ item.price | number_format }} Points</a></h2>
|
||||
</div>
|
||||
<form action="bonus.php?action=purchase&label={{label}}" method="post">
|
||||
<form name="bonus-other" action="bonus.php" method="post">
|
||||
<div class="pad">
|
||||
<table cellpadding="6" cellspacing="1" border="0" class="layout border" width="100%">
|
||||
<tbody>
|
||||
<input type="hidden" name="auth" value="{{auth}}" />
|
||||
<input type="hidden" name="confirm" value="true" />
|
||||
<input type="hidden" name="action" value="purchase" />
|
||||
<input type="hidden" name="auth" value="{{ viewer.auth }}" />
|
||||
<input type="hidden" name="label" value="{{ item.label }}" />
|
||||
<input type="hidden" name="user" value="" />
|
||||
<tr>
|
||||
<td class="label">Username:</td>
|
||||
<td>
|
||||
<input type="text" id="user" name="user" style="width:90%" /> <br />
|
||||
<input type="text" id="bonus-user-other" name="bonus-user-other" length="20" />
|
||||
<span id="bonus-other-status"></span>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="label">Message:</td>
|
||||
<td>
|
||||
<div style="width:90%">
|
||||
{{ textarea.preview|raw }}
|
||||
{{ textarea.field|raw }}
|
||||
{{ message.preview|raw }}
|
||||
{{ message.field|raw }}
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<div id="buttons" class="center">
|
||||
{{ textarea.button|raw }}
|
||||
<input type="submit" onclick="ConfirmPurchase(event,'{{item}}')" value="Submit" />
|
||||
{{ message.button|raw }}
|
||||
<input type="submit" id="bonus-other-purchase" disabled="disabled" value="Purchase" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</form>
|
||||
</div>
|
||||
{{ footer() }}
|
||||
|
||||
@@ -83,37 +83,30 @@
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{% if user.isEnabled and (not user.hasAttr('no-fl-gifts')) and (freeleech.item or freeleech.other) %}
|
||||
{% if user.isEnabled and (not user.hasAttr('no-fl-gifts')) and freeleech.offer %}
|
||||
<div class="box box_info box_userinfo_give_FL">
|
||||
{% if freeleech.other %}
|
||||
<div class="head colhead_dark">Freeleech Tokens Given</div>
|
||||
<ul class="stats nobullet">
|
||||
{% if freeleech.other > 0 %}
|
||||
<li>You gave {{ freeleech.other }} token{{ freeleech.other|plural }} to {{ user.username }}. Your generosity is most appreciated!</li>
|
||||
{% else %}
|
||||
<li>You attempted to give some tokens to {{ user.username }} but something didn't work out.
|
||||
No points were spent.</li>
|
||||
{% endif %}
|
||||
</ul>
|
||||
{% else %}
|
||||
<div class="head colhead_dark">Give Freeleech Tokens</div>
|
||||
<form class="fl_form" name="user" id="fl_form" action="user.php?id={{ user_id }}" method="post">
|
||||
<ul class="stats nobullet">
|
||||
{% for f in freeleech.item %}
|
||||
<li><input type="radio" name="fltype" id="fl-{{ f.Label }}" value="fl-{{ f.Label }}" />
|
||||
<label title="This costs {{ f.Price|number_format }} BP, which will leave you with {{ f.After|number_format
|
||||
}} afterwards" for="fl-{{ f.Label }}"> {{ f.Name }}</label></li>
|
||||
{% for item in freeleech.offer %}
|
||||
<li><input type="radio" name="fltype" id="{{ item.label }}" value="{{ item.label }}" />
|
||||
<label title="This costs {{ item.price|number_format }} BP, which will leave you with {{
|
||||
(viewer.bonusPointsTotal - item.price)|number_format}} afterwards" for="{{
|
||||
item.label }}"> {{ item.title }}</label></li>
|
||||
{% endfor %}
|
||||
<li><input type="text" id="message" name="message" placeholder="Message"/> <br /></li>
|
||||
<li><input type="submit" name="flsubmit" value="Send" /></li>
|
||||
{% if freeleech.latest %}
|
||||
<li>(You gave them {{ freeleech.latest.title|trim(' to Other') }} {{ freeleech.latest.purchase_date|time_diff }})</li>
|
||||
{% set when = freeleech.latest.purchase_date|time_diff %}
|
||||
<li>
|
||||
You gave them {{ freeleech.latest.title|trim(' to Other') }} {{ when|raw }}
|
||||
{%- if 'Just now' in when %}. Your generosity is most appreciated!{% endif -%}
|
||||
</li>
|
||||
{% endif %}
|
||||
</ul>
|
||||
<input type="hidden" name="action" value="fltoken" />
|
||||
<input type="hidden" name="auth" value="{{ viewer.auth }}" />
|
||||
</form>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
|
||||
@@ -3,7 +3,9 @@
|
||||
namespace Gazelle;
|
||||
|
||||
use PHPUnit\Framework\TestCase;
|
||||
use PHPUnit\Framework\Attributes\DataProvider;
|
||||
use GazelleUnitTest\Helper;
|
||||
use Gazelle\Enum\BonusItemPurchaseStatus;
|
||||
use Gazelle\Enum\UserStatus;
|
||||
|
||||
class BonusTest extends TestCase {
|
||||
@@ -23,12 +25,13 @@ class BonusTest extends TestCase {
|
||||
}
|
||||
}
|
||||
|
||||
public function testBonus(): void {
|
||||
$this->userList['giver'] = Helper::makeUser('bonusg.' . randomString(6), 'bonus', true);
|
||||
public function testBonusBasic(): void {
|
||||
$this->userList['giver'] = Helper::makeUser('bonusg.' . randomString(6), 'bonus', true);
|
||||
$this->userList['receiver'] = Helper::makeUser('bonusr.' . randomString(6), 'bonus', true);
|
||||
$startingPoints = 10000;
|
||||
|
||||
$giver = new User\Bonus($this->userList['giver']);
|
||||
$user = $this->userList['giver'];
|
||||
$giver = new User\Bonus($user);
|
||||
$this->assertEquals(0.0, $giver->hourlyRate(), 'bonus-per-hour');
|
||||
$this->assertEquals(0, $giver->user()->bonusPointsTotal(), 'bonus-points-initial');
|
||||
$this->assertEquals(0, $giver->user()->tokenCount(), 'bonus-fltokens-intial');
|
||||
@@ -50,93 +53,337 @@ class BonusTest extends TestCase {
|
||||
);
|
||||
|
||||
$giver->setPoints($startingPoints);
|
||||
$this->assertEquals($startingPoints, $giver->user()->bonusPointsTotal(), 'bonus-set-points');
|
||||
$this->assertEquals(
|
||||
$startingPoints,
|
||||
$giver->user()->bonusPointsTotal(),
|
||||
'bonus-set-points',
|
||||
);
|
||||
|
||||
$manager = new Manager\Bonus();
|
||||
$manager->flushPriceCache();
|
||||
|
||||
$itemList = $manager->itemList();
|
||||
$this->assertArrayHasKey('token-1', $itemList, 'item-token-1');
|
||||
$token = $giver->item('token-1');
|
||||
$this->assertArrayHasKey('Price', $token, 'item-price-1');
|
||||
$price = $token['Price'];
|
||||
$this->assertEquals($price, $giver->effectivePrice('token-1'), 'item-price-token-1');
|
||||
$manager->flush();
|
||||
$this->assertCount(13, $manager->itemList(), 'bonus-item-list');
|
||||
$this->assertNull($manager->findBonusItemByLabel('nope'), 'bonus-item-null');
|
||||
|
||||
// buy a token
|
||||
$this->assertTrue($giver->purchaseToken('token-1'), 'item-purchase-token-1');
|
||||
$this->assertEquals(1, $giver->user()->tokenCount(), 'bonus-fltokens-bought');
|
||||
$this->assertEquals($startingPoints - $price, $giver->user()->bonusPointsTotal(), 'bonus-spent-points');
|
||||
$flt = $manager->findBonusItemByLabel('token-1');
|
||||
$this->assertFalse($flt->needsPreparation(), 'bonus-token-no-prepare');
|
||||
$this->assertInstanceOf(BonusItem::class, $flt->flush(), 'bonus-item-flush');
|
||||
$this->assertEquals('', $flt->link(), 'bonus-item-link');
|
||||
$this->assertEquals('bonus.php', $flt->location(), 'bonus-item-location');
|
||||
$this->assertEquals(
|
||||
BonusItemPurchaseStatus::success,
|
||||
$flt->purchase($user, $flt->price()),
|
||||
'bonus-item-purchase-token-1',
|
||||
);
|
||||
$this->assertEquals(
|
||||
1,
|
||||
$giver->user()->tokenCount(),
|
||||
'bonus-fltokens-bought',
|
||||
);
|
||||
$this->assertEquals(
|
||||
$startingPoints - $flt->price(),
|
||||
$giver->user()->bonusPointsTotal(),
|
||||
'bonus-spent-points',
|
||||
);
|
||||
$history = $giver->history(10, 0);
|
||||
$this->assertEquals('1 Freeleech Token', $history[0]['Title'], 'bonus-history-title');
|
||||
$this->assertEquals(
|
||||
'1 Freeleech Token',
|
||||
$history[0]['Title'],
|
||||
'bonus-history-title',
|
||||
);
|
||||
|
||||
// buy a seedbox
|
||||
$this->assertTrue($giver->unlockSeedbox(), 'item-purchase-seedbox');
|
||||
$seedbox = $manager->findBonusItemByLabel('seedbox');
|
||||
$this->assertEquals(
|
||||
'Seedbox Viewer',
|
||||
$seedbox->title(),
|
||||
'bonus-item-title',
|
||||
);
|
||||
$this->assertEquals(
|
||||
BonusItemPurchaseStatus::forbidden,
|
||||
$seedbox->purchase($user, $seedbox->price()),
|
||||
'bonus-item-purchase-seedbox-forbidden',
|
||||
);
|
||||
$user->setField('PermissionID', MEMBER)->modify();
|
||||
$this->assertEquals(
|
||||
BonusItemPurchaseStatus::success,
|
||||
$seedbox->purchase($user, $seedbox->price()),
|
||||
'bonus-item-purchase-seedbox-success',
|
||||
);
|
||||
$this->assertTrue($giver->user()->hasAttr('feature-seedbox'), 'giver-has-seedbox');
|
||||
$this->assertCount(2, $giver->history(10, 0), 'bonus-history-new');
|
||||
$this->assertFalse($seedbox->isRecurring(), 'bonus-item-recurring');
|
||||
$this->assertEquals(
|
||||
BonusItemPurchaseStatus::alreadyPurchased,
|
||||
$seedbox->purchase($user, 0),
|
||||
'bonus-item-purchase-seedbox-duplicate',
|
||||
);
|
||||
|
||||
// not enough point to buy a fifty
|
||||
$this->assertFalse($giver->purchaseToken('token-50'), 'item-purchase-token-50');
|
||||
$flt50 = $manager->findBonusItemByLabel('token-3');
|
||||
$this->assertEquals(
|
||||
BonusItemPurchaseStatus::insufficientFunds,
|
||||
$flt50->purchase($user, $flt50->price()),
|
||||
'bonus-item-purchase-token-50',
|
||||
);
|
||||
|
||||
$other1 = $manager->findBonusItemByLabel('other-1');
|
||||
$this->assertTrue($other1->needsPreparation(), 'bonus-token-other-prepare');
|
||||
$other50 = $manager->findBonusItemByLabel('other-3');
|
||||
$giver->addPoints(
|
||||
(float)($giver->item('other-1')['Price'])
|
||||
+ (float)($giver->item('other-3')['Price'])
|
||||
(float)($other1->price() + $other50->price())
|
||||
);
|
||||
$this->assertEquals(
|
||||
$giver->item('other-3')['Amount'],
|
||||
$giver->purchaseTokenOther($this->userList['receiver'], 'other-3', 'phpunit gift'),
|
||||
'item-purchase-other-50'
|
||||
BonusItemPurchaseStatus::incomplete,
|
||||
$other50->purchase(
|
||||
$user,
|
||||
$other50->price(),
|
||||
),
|
||||
'bonus-item-purchase-incomplete-other-50',
|
||||
);
|
||||
$other = $giver->otherList();
|
||||
$this->assertEquals('other-1', $other[0]['Label'], 'item-all-I-can-give');
|
||||
$this->userList['receiver']->toggleAttr('no-fl-gifts', true);
|
||||
$this->assertEquals(
|
||||
BonusItemPurchaseStatus::declined,
|
||||
$other50->purchase(
|
||||
$user,
|
||||
$other50->price(),
|
||||
[
|
||||
'message' => 'phpunit gift',
|
||||
'receiver' => $this->userList['receiver'],
|
||||
],
|
||||
),
|
||||
'bonus-item-purchase-declined-other-50',
|
||||
);
|
||||
$this->userList['receiver']->toggleAttr('no-fl-gifts', false);
|
||||
$this->assertEquals(
|
||||
BonusItemPurchaseStatus::success,
|
||||
$other50->purchase(
|
||||
$user,
|
||||
$other50->price(),
|
||||
[
|
||||
'message' => 'phpunit gift',
|
||||
'receiver' => $this->userList['receiver'],
|
||||
],
|
||||
),
|
||||
'bonus-item-purchase-other-50',
|
||||
);
|
||||
$offer = $manager->offerTokenOther($user);
|
||||
$this->assertEquals('other-1', $offer[0]->label(), 'bonus-item-all-I-can-give');
|
||||
|
||||
// buy file count feature
|
||||
$giver->addPoints(
|
||||
(float)($giver->item('file-count')['Price'])
|
||||
$fileCount = $manager->findBonusItemByLabel('file-count');
|
||||
$giver->addPoints((float)$fileCount->price());
|
||||
$this->assertEquals(
|
||||
BonusItemPurchaseStatus::success,
|
||||
$fileCount->purchase($user, $fileCount->price()),
|
||||
'bonus-item-purchase-file-count',
|
||||
);
|
||||
$this->assertTrue($user->hasAttr('feature-file-count'), 'giver-has-file-count');
|
||||
$this->assertEquals(
|
||||
BonusItemPurchaseStatus::alreadyPurchased,
|
||||
$fileCount->purchase($user, $fileCount->price()),
|
||||
'bonus-item-repurchase-file-count',
|
||||
);
|
||||
$this->assertTrue($giver->purchaseFeatureFilecount(), 'item-purchase-file-count');
|
||||
$this->assertTrue($giver->user()->hasAttr('feature-seedbox'), 'giver-has-file-count');
|
||||
|
||||
$this->assertEquals(
|
||||
$giver->item('token-1')['Price'] /** @phpstan-ignore-line it is an int, get over it */
|
||||
+ $giver->item('other-3')['Price']
|
||||
+ $giver->item('seedbox')['Price']
|
||||
+ $giver->item('file-count')['Price'],
|
||||
$flt->price()
|
||||
+ $other50->price()
|
||||
+ $seedbox->price()
|
||||
+ $fileCount->price(),
|
||||
$giver->pointsSpent(),
|
||||
'bonus-points-spent'
|
||||
'bonus-points-spent',
|
||||
);
|
||||
|
||||
$latest = $giver->otherLatest($this->userList['receiver']);
|
||||
$this->assertEquals('50 Freeleech Tokens to Other', $latest['title'], 'item-given');
|
||||
$this->assertEquals('50 Freeleech Tokens to Other', $latest['title'], 'bonus-item-given');
|
||||
|
||||
$giver->addPoints($giver->item('title-bb-n')['Price']);
|
||||
$this->assertTrue($giver->purchaseTitle('title-bb-n', '[b]i got u[/b]'), 'item-title-no-bb');
|
||||
$this->assertEquals('i got u', $giver->user()->title(), 'item-user-has-title-no-bb');
|
||||
$bbn = $manager->findBonusItemByLabel('title-bb-n');
|
||||
$giver->addPoints($bbn->price());
|
||||
$this->assertEquals(
|
||||
BonusItemPurchaseStatus::success,
|
||||
$bbn->purchase($user, $bbn->price(), ['title' => '[b]i got u[/b]']),
|
||||
'bonus-item-title-no-bb',
|
||||
);
|
||||
$this->assertEquals('i got u', $user->title(), 'bonus-item-user-has-title-no-bb');
|
||||
|
||||
$giver->addPoints($giver->item('title-bb-y')['Price']);
|
||||
$this->assertTrue($giver->purchaseTitle('title-bb-y', '[b]i got u[/b]'), 'item-title-yes-bb');
|
||||
$this->assertEquals('<strong>i got u</strong>', $giver->user()->title(), 'item-user-has-title-yes-bb');
|
||||
$bby = $manager->findBonusItemByLabel('title-bb-y');
|
||||
$giver->addPoints($bby->price());
|
||||
$this->assertEquals(
|
||||
BonusItemPurchaseStatus::success,
|
||||
$bby->purchase($user, $bby->price(), ['title' => '[b]i got u[/b]']),
|
||||
'bonus-item-title-yes-bb',
|
||||
);
|
||||
$this->assertEquals(
|
||||
'<strong>i got u</strong>',
|
||||
$user->title(),
|
||||
'bonus-item-user-has-title-yes-bb',
|
||||
);
|
||||
|
||||
$giver->addPoints($giver->item('collage-1')['Price']);
|
||||
$this->assertTrue($giver->purchaseCollage('collage-1'), 'item-purchase-collage');
|
||||
$off = $manager->findBonusItemByLabel('title-off');
|
||||
$this->assertEquals(
|
||||
BonusItemPurchaseStatus::success,
|
||||
$off->purchase($user, $off->price()),
|
||||
'bonus-item-title-off',
|
||||
);
|
||||
$this->assertEquals(
|
||||
'',
|
||||
$user->title(),
|
||||
'bonus-item-user-has-no-title',
|
||||
);
|
||||
|
||||
$collage = $manager->findBonusItemByLabel('collage-1');
|
||||
$giver->addPoints($collage->price());
|
||||
$this->assertEquals(
|
||||
BonusItemPurchaseStatus::success,
|
||||
$collage->purchase($user, $collage->price()),
|
||||
'bonus-item-purchase-collage',
|
||||
);
|
||||
$this->assertEquals(
|
||||
$collage->price() * 2,
|
||||
$collage->priceForUser($giver->user()),
|
||||
'bonus-item-price-for-user'
|
||||
);
|
||||
$this->assertEquals(
|
||||
$collage->price(),
|
||||
$collage->priceForUser($this->userList['receiver']),
|
||||
'bonus-item-price-for-other-user'
|
||||
);
|
||||
|
||||
$history = $giver->history(10, 0);
|
||||
$this->assertCount(7, $history, 'bonus-history-final');
|
||||
$this->assertCount(8, $history, 'bonus-history-final');
|
||||
|
||||
$this->assertEquals(
|
||||
[
|
||||
'nr' => 7,
|
||||
'total' => $giver->item('token-1')['Price'] /** @phpstan-ignore-line */
|
||||
+ $giver->item('other-3')['Price']
|
||||
+ $giver->item('seedbox')['Price']
|
||||
+ $giver->item('file-count')['Price']
|
||||
+ $giver->item('collage-1')['Price']
|
||||
+ $giver->item('title-bb-y')['Price']
|
||||
+ $giver->item('title-bb-n')['Price']
|
||||
'nr' => 8,
|
||||
'total' => $flt->price()
|
||||
+ $other50->price()
|
||||
+ $seedbox->price()
|
||||
+ $fileCount->price()
|
||||
+ $collage->price()
|
||||
+ $bbn->price()
|
||||
+ $bby->price()
|
||||
+ $off->price()
|
||||
],
|
||||
$giver->summary(),
|
||||
'bonus-summary-initial'
|
||||
);
|
||||
$this->assertTrue($giver->removePoints(1.125), 'bonus-taketh-away');
|
||||
|
||||
$this->assertEquals(
|
||||
BonusItemPurchaseStatus::insufficientFunds,
|
||||
$bby->purchase($user, $bby->price(), ['title' => 'whatever']),
|
||||
'bonus-item-no-funds-title-yes-bb',
|
||||
);
|
||||
// higher userclasses get some items for free
|
||||
$user->setField('PermissionID', SYSOP)->modify();
|
||||
$this->assertEquals(
|
||||
BonusItemPurchaseStatus::success,
|
||||
$bby->purchase($user, $bby->price(), ['title' => 'whatever']),
|
||||
'bonus-item-free-title-yes-bb',
|
||||
);
|
||||
$this->assertEquals(
|
||||
BonusItemPurchaseStatus::incomplete,
|
||||
$bby->purchase($user, $bby->price()),
|
||||
'bonus-item-free-title-yes-bb',
|
||||
);
|
||||
|
||||
$userBonus = new User\Bonus($user);
|
||||
$history = $userBonus->purchaseHistory();
|
||||
$this->assertCount(8, $history, 'bonus-history-count');
|
||||
$this->assertEquals(
|
||||
['id', 'title', 'total', 'cost'],
|
||||
array_keys(current($history)),
|
||||
'bonus-history-shape',
|
||||
);
|
||||
$this->assertCount(0, $userBonus->seedList(5, 0), 'bonus-history-seedlist');
|
||||
$this->assertCount(0, $userBonus->poolHistory(), 'bonus-history-pool');
|
||||
|
||||
// Here is as good a place as any
|
||||
$this->assertEquals(0, $manager->discount(), 'bonus-discount');
|
||||
}
|
||||
|
||||
public static function providerBonusItem(): array {
|
||||
return [
|
||||
['collage-1'],
|
||||
['file-count'],
|
||||
['invite'],
|
||||
['seedbox'],
|
||||
['other-1'],
|
||||
['other-2'],
|
||||
['other-3'],
|
||||
['token-1'],
|
||||
['token-2'],
|
||||
['token-3'],
|
||||
];
|
||||
}
|
||||
|
||||
#[DataProvider('providerBonusItem')]
|
||||
public function testBonusBroke(string $label): void {
|
||||
$this->userList = [Helper::makeUser('bonusg.' . randomString(6), 'bonus', true)];
|
||||
$this->userList[0]->setField('PermissionID', MEMBER)->modify();
|
||||
$item = new Manager\Bonus()->findBonusItemByLabel($label);
|
||||
if (str_starts_with($label, 'other-')) {
|
||||
$this->userList[] = Helper::makeUser('bonusg.' . randomString(6), 'bonus', true);
|
||||
$this->assertEquals(
|
||||
BonusItemPurchaseStatus::insufficientFunds,
|
||||
$item->purchase($this->userList[0], $item->price(), ['receiver' => $this->userList[1]]),
|
||||
"bonus-item-broke-$label",
|
||||
);
|
||||
} else {
|
||||
$this->assertEquals(
|
||||
BonusItemPurchaseStatus::insufficientFunds,
|
||||
$item->purchase($this->userList[0], $item->price()),
|
||||
"bonus-item-broke-$label",
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
public function testBonusPurchaseOther(): void {
|
||||
$this->userList['giver'] = Helper::makeUser('bonusg.' . randomString(6), 'bonus', true);
|
||||
$this->userList['receiver'] = Helper::makeUser('bonusr.' . randomString(6), 'bonus', true);
|
||||
$manager = new Manager\Bonus();
|
||||
|
||||
$this->assertEquals(
|
||||
BonusItemPurchaseStatus::incomplete,
|
||||
$manager->purchaseTokenOther(
|
||||
$this->userList['giver'],
|
||||
$this->userList['receiver'],
|
||||
'bad-label',
|
||||
'',
|
||||
),
|
||||
'bonus-purchase-token-other-fail',
|
||||
);
|
||||
$this->assertEquals(
|
||||
BonusItemPurchaseStatus::insufficientFunds,
|
||||
$manager->purchaseTokenOther(
|
||||
$this->userList['giver'],
|
||||
$this->userList['receiver'],
|
||||
'other-1',
|
||||
'',
|
||||
),
|
||||
'bonus-purchase-token-other-no-money',
|
||||
);
|
||||
|
||||
$this->assertCount(0,
|
||||
$manager->offerTokenOther($this->userList['giver']),
|
||||
'bonus-offer-other-none',
|
||||
);
|
||||
new User\Bonus($this->userList['giver'])->addPoints(2000000);
|
||||
$this->assertCount(
|
||||
3,
|
||||
$manager->offerTokenOther($this->userList['giver']),
|
||||
'bonus-offer-other-all',
|
||||
);
|
||||
$this->assertEquals(
|
||||
BonusItemPurchaseStatus::success,
|
||||
$manager->purchaseTokenOther(
|
||||
$this->userList['giver'],
|
||||
$this->userList['receiver'],
|
||||
'other-1',
|
||||
'',
|
||||
),
|
||||
'bonus-purchase-token-other-success',
|
||||
);
|
||||
}
|
||||
|
||||
public function testBonusPool(): void {
|
||||
@@ -150,6 +397,94 @@ class BonusTest extends TestCase {
|
||||
);
|
||||
}
|
||||
|
||||
public function testBonusInvite(): void {
|
||||
$user = Helper::makeUser('bonusinvite.' . randomString(6), 'bonus');
|
||||
$this->userList = [$user];
|
||||
|
||||
$invite = new Manager\Bonus()->findBonusItemByLabel('invite');
|
||||
$this->assertTrue($invite->permitted($user), 'bonus-invite-yes');
|
||||
$user->toggleAttr('disable-invites', true);
|
||||
$this->assertFalse($invite->permitted($user), 'bonus-invite-no');
|
||||
|
||||
$userBonus = new User\Bonus($user);
|
||||
$this->assertTrue(
|
||||
$userBonus->removePoints(1000, force: true),
|
||||
'bonus-remove-force'
|
||||
);
|
||||
$this->assertEquals(
|
||||
-1000,
|
||||
$user->bonusPointsTotal(),
|
||||
'bonus-points-negative'
|
||||
);
|
||||
}
|
||||
|
||||
public function testBonusTitle(): void {
|
||||
$this->assertEquals(
|
||||
[],
|
||||
new Json\BonusItemTitle('nope', 'title')->payload(),
|
||||
'bonus-title-bad',
|
||||
);
|
||||
$this->assertEquals(
|
||||
['phpunit'],
|
||||
new Json\BonusItemTitle('title-bb-n', '[b]phpunit[/b]')->payload(),
|
||||
'bonus-title-plain',
|
||||
);
|
||||
$this->assertEquals(
|
||||
['<strong>phpunit</strong>'],
|
||||
new Json\BonusItemTitle('title-bb-y', '[b]phpunit[/b]')->payload(),
|
||||
'bonus-title-rich',
|
||||
);
|
||||
}
|
||||
|
||||
public function testBonusUserOther(): void {
|
||||
$user = Helper::makeUser('bonusother.' . randomString(6), 'bonus', enable: false);
|
||||
$this->userList = [$user];
|
||||
|
||||
$this->assertEquals(
|
||||
[
|
||||
'found' => false,
|
||||
'username' => '#nope',
|
||||
],
|
||||
new Json\BonusUserOther('#nope')->payload(),
|
||||
'bonus-user-other-404',
|
||||
);
|
||||
$this->assertEquals(
|
||||
[
|
||||
'found' => true,
|
||||
'accept' => true,
|
||||
'enabled' => false,
|
||||
'id' => $user->id,
|
||||
'username' => $user->username(),
|
||||
],
|
||||
new Json\BonusUserOther($user->username())->payload(),
|
||||
'bonus-user-other-not-enabled',
|
||||
);
|
||||
$user->setField('Enabled', UserStatus::enabled->value)->modify();
|
||||
$this->assertEquals(
|
||||
[
|
||||
'found' => true,
|
||||
'accept' => true,
|
||||
'enabled' => true,
|
||||
'id' => $user->id,
|
||||
'username' => $user->username(),
|
||||
],
|
||||
new Json\BonusUserOther($user->username())->payload(),
|
||||
'bonus-user-other-accept',
|
||||
);
|
||||
$user->toggleAttr('no-fl-gifts', true);
|
||||
$this->assertEquals(
|
||||
[
|
||||
'found' => true,
|
||||
'accept' => false,
|
||||
'enabled' => true,
|
||||
'id' => $user->id,
|
||||
'username' => $user->username(),
|
||||
],
|
||||
new Json\BonusUserOther($user->username())->payload(),
|
||||
'bonus-user-other-no-fl',
|
||||
);
|
||||
}
|
||||
|
||||
public function testAddPoints(): void {
|
||||
$this->userList = [
|
||||
Helper::makeUser('bonusadd.' . randomString(6), 'bonus', enable: false),
|
||||
|
||||
@@ -35,36 +35,45 @@ class InviteTest extends TestCase {
|
||||
$this->assertEquals(0, $this->user->invite()->pendingTotal(), 'invite-pending-0-initial');
|
||||
|
||||
// USER cannot invite, but MEMBER can
|
||||
$this->assertFalse($this->user->canInvite(), 'invite-cannot-invite');
|
||||
$this->assertFalse($this->user->canPurchaseInvite(), 'invite-cannot-purchase');
|
||||
$this->assertFalse($this->user->canInvite(), 'inviter-cannot-invite');
|
||||
$this->assertFalse($this->user->canPurchaseInvite(), 'inviter-cannot-purchase');
|
||||
$this->user->setField('PermissionID', MEMBER)->modify();
|
||||
$this->assertTrue($this->user->canInvite(), 'invite-can-invite');
|
||||
$this->assertTrue($this->user->canPurchaseInvite(), 'invite-can-now-purchase');
|
||||
$this->assertTrue($this->user->canInvite(), 'inviter-can-invite');
|
||||
$this->assertTrue($this->user->canPurchaseInvite(), 'inviter-can-now-purchase');
|
||||
|
||||
// add some BP to play with
|
||||
$bonus = new User\Bonus($this->user);
|
||||
$this->assertEquals(1, $bonus->addPoints(1_000_000), 'invite-add-bp');
|
||||
$this->assertTrue($bonus->purchaseInvite(), 'invite-purchase-invite');
|
||||
$this->assertEquals(1, $this->user->unusedInviteTotal(), 'invite-unused-1');
|
||||
$this->assertEquals(1, $bonus->addPoints(1_000_000), 'inviter-add-bp');
|
||||
$invite = new Manager\Bonus()->findBonusItemByLabel('invite');
|
||||
$this->assertEquals(
|
||||
Enum\BonusItemPurchaseStatus::success,
|
||||
$invite->purchase($this->user, $invite->price()),
|
||||
'inviter-purchase-invite'
|
||||
);
|
||||
$this->assertEquals(1, $this->user->unusedInviteTotal(), 'inviter-unused-1');
|
||||
|
||||
$this->assertTrue($this->user->invite()->issueInvite(), 'invite-issue-true');
|
||||
$this->assertFalse($this->user->invite()->issueInvite(), 'invite-decrement-none-left');
|
||||
$this->assertTrue($this->user->invite()->issueInvite(), 'inviter-issue-true');
|
||||
$this->assertFalse($this->user->invite()->issueInvite(), 'inviter-decrement-none-left');
|
||||
$this->user->setField('Invites', 1)->modify();
|
||||
|
||||
// invite someone
|
||||
$this->assertTrue(new Stats\Users()->newUsersAllowed($this->user), 'invite-new-users-allowed');
|
||||
$this->assertTrue(new Stats\Users()->newUsersAllowed($this->user), 'inviter-new-users-allowed');
|
||||
$manager = new Manager\Invite();
|
||||
$email = randomString(10) . "@invitee.example.com";
|
||||
$this->assertFalse($manager->emailExists($this->user, $email), 'invitee-email-not-pending');
|
||||
$invite = $manager->create($this->user, $email, 'unittest notes', 'unittest reason', '');
|
||||
$this->assertInstanceOf(Invite::class, $invite, 'invite-invitee-created');
|
||||
$this->assertEquals(1, $this->user->invite()->pendingTotal(), 'invite-pending-1');
|
||||
$this->assertEquals(0, $this->user->unusedInviteTotal(), 'invite-unused-0-again');
|
||||
$this->assertEquals($invite->email(), $this->user->invite()->pendingList()[$invite->key()]['email'], 'invite-invitee-email');
|
||||
$invitation = $manager->create($this->user, $email, 'unittest notes', 'unittest reason', '');
|
||||
$this->assertInstanceOf(Invite::class, $invitation, 'inviter-invitee-created');
|
||||
$this->assertEquals(1, $this->user->invite()->pendingTotal(), 'inviter-pending-1');
|
||||
$this->assertEquals(0, $this->user->unusedInviteTotal(), 'inviter-unused-0-again');
|
||||
$this->assertEquals(
|
||||
$invitation->email(),
|
||||
$this->user->invite()->pendingList()[$invitation->key()]['email'],
|
||||
'inviter-invitee-email',
|
||||
);
|
||||
|
||||
// respond to invite
|
||||
$this->assertTrue($manager->inviteExists($invite->key()), 'invite-key-found');
|
||||
$this->invitee = Helper::makeUserByInvite('invitee.' . randomString(6), $invite->key());
|
||||
$this->assertTrue($manager->inviteExists($invitation->key()), 'invite-key-found');
|
||||
$this->invitee = Helper::makeUserByInvite('invitee.' . randomString(6), $invitation->key());
|
||||
$this->assertInstanceOf(User::class, $this->invitee, 'invitee-class');
|
||||
$this->assertEquals($this->user->id, $this->invitee->inviter()?->id, 'invitee-invited-by');
|
||||
$this->assertEquals($this->user->id, $this->invitee->inviterId(), 'invitee-invited-id');
|
||||
|
||||
@@ -115,7 +115,12 @@ class TGroupTest extends TestCase {
|
||||
|
||||
$bonus = (new User\Bonus($this->userList['user']));
|
||||
$this->assertEquals(1, $bonus->addPoints(10000), 'tgroup-user-add-bp');
|
||||
$this->assertEquals(1, $bonus->purchaseToken('token-1'), 'tgroup-user-buy-token');
|
||||
$token = new Manager\Bonus()->findBonusItemByLabel('token-1');
|
||||
$this->assertEquals(
|
||||
Enum\BonusItemPurchaseStatus::success,
|
||||
$token->purchase($this->userList['user'], $token->price()),
|
||||
'tgroup-user-buy-fltoken'
|
||||
);
|
||||
$this->assertTrue($this->userList['user']->canSpendFLToken($torrent), 'tgroup-user-fltoken');
|
||||
|
||||
new Stats\Users()->refresh();
|
||||
|
||||
Reference in New Issue
Block a user