dynamic pricing for purchasing tokens for other

This commit is contained in:
Spine
2025-09-27 05:25:51 +00:00
parent 6129a3912d
commit 8787b9f80b
8 changed files with 277 additions and 176 deletions

View File

@@ -77,6 +77,28 @@ class BonusItem extends \Gazelle\BaseObject {
return $this->info()['price'];
}
public function priceForTokenOther(User $giver, User $receiver): int|false {
if (!str_starts_with($this->label(), 'other-')) {
return false;
}
// Inviters send tokens at a flat rate.
if ($receiver->inviterId() === $giver->id) {
return $this->price();
}
$exchange = new User\Bonus($receiver)->tokenExchange();
// For every BONUS_OTHER_TOKEN_INTERVAL received,
// scale the price up by BONUS_OTHER_TOKEN_SCALE percent.
return (int)ceil(
$this->price()
* pow(
1 + (BONUS_OTHER_TOKEN_SCALE / 100),
floor(max(0, $exchange['received'] - $exchange['sent'])
/ BONUS_OTHER_TOKEN_INTERVAL
)
)
);
}
public function priceForUser(User $user): int {
return match ($this->label()) {
'collage-1' => $this->price() * 2 ** min(6, $user->paidPersonalCollages()),

View File

@@ -3,26 +3,38 @@
namespace Gazelle\Json;
use Gazelle\Manager\User as UserManager;
use Gazelle\BonusItem as BonusItem;
use Gazelle\User as User;
class BonusUserOther extends \Gazelle\Json {
public function __construct(
protected User $user,
protected BonusItem $item,
protected string $username,
protected UserManager $manager = new UserManager(),
) {}
public function payload(): array {
$other = $this->manager->findByUsername($this->username);
return is_null($other)
? [
if (is_null($other)) {
return [
'found' => false,
'username' => $this->username,
]
: [
'found' => true,
'accept' => !$other->hasAttr('no-fl-gifts'),
'enabled' => $other->isEnabled(),
'id' => $other->id,
'username' => $other->username(),
];
}
$points = $this->user->bonusPointsTotal();
$price = $this->item->priceForTokenOther($this->user, $other);
if ($price === false) {
json_error("bad item label received");
}
return [
'found' => true,
'accept' => !$other->hasAttr('no-fl-gifts'),
'enabled' => $other->isEnabled(),
'id' => $other->id,
'price' => $price,
'percent5' => $points === 0 ? 0 : ceil(($price / $points) * 20) * 5,
'username' => $other->username(),
];
}
}

View File

@@ -175,7 +175,10 @@ foreach ($torrentList as $torrentId) {
$torrent = $torMan->findById((int)$torrentId);
if ($torrent) {
echo "torrent $torrentId\n";
$torrent->removeTorrent(null, 'garbage collection', -1);
try {
$torrent->removeTorrent(null, 'garbage collection', -1);
} catch (\Exception) {
}
}
}
@@ -183,12 +186,15 @@ $tgMan = new Manager\TGroup();
foreach ($groupList as $tgroupId) {
$tgroup = $tgMan->findById($tgroupId);
if ($tgroup instanceof TGroup) {
echo "tgroup $torrentId ({$tgroup?->name()})\n";
$db->prepared_query("
DELETE FROM bookmarks_torrents bt WHERE GroupID = ?
", $tgroupId
);
$tgroup->remove(new User(1));
try {
echo "tgroup $torrentId ({$tgroup?->name()})\n";
$db->prepared_query("
DELETE FROM bookmarks_torrents bt WHERE GroupID = ?
", $tgroupId
);
$tgroup->remove(new User(1));
} catch (\Error|\Exception) {
}
}
}

View File

@@ -734,6 +734,11 @@ defined('BONUS_POOL_TAX_ELITE') or define('BONUS_POOL_TAX_ELITE', 0.8);
defined('BONUS_POOL_TAX_TM') or define('BONUS_POOL_TAX_TM', 0.7);
defined('BONUS_POOL_TAX_STAFF') or define('BONUS_POOL_TAX_STAFF', 0.5);
// Pricing of tokens to other scales up at every interval of tokens received.
defined('BONUS_OTHER_TOKEN_INTERVAL') or define('BONUS_OTHER_TOKEN_INTERVAL', 100);
// At each interval, prices are raised by SCALE percent. Set to 0 to disable scaling.
defined('BONUS_OTHER_TOKEN_SCALE') or define('BONUS_OTHER_TOKEN_SCALE', 20);
// ------------------------------------------------------------------------
// Pagination

View File

@@ -30,6 +30,7 @@ async function validateBonusUsername() {
const form = new FormData();
form.append('auth', document.body.dataset.auth);
form.append('bonus-user-other', username);
form.append('label', document.forms['bonus-other'].elements['label'].value);
const response = await fetch(
'?action=prepare', {
'method': 'POST',
@@ -41,18 +42,21 @@ async function validateBonusUsername() {
if (data.status !== 'success') {
message = status;
} else {
const user = data.response;
if (!user.found) {
const info = data.response;
if (!info.found) {
message = '⛔️ ' + 'user not found';
} else if (user.id == document.body.dataset.id) {
} else if (info.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 if (!info.enabled) {
message = '⛔️ ' + info.username + ' is currently not enabled';
} else if (!info.accept) {
message = '🚫 ' + info.username + ' does not wish to receive tokens';
} else {
message = '✅';
document.forms['bonus-other'].elements['user'].value = user.username;
message = '✅ This will cost ' + info.price + ' bonus points';
if (info.percent5 > 0) {
message = message + ' (which is more than ' + info.percent5 + '% of your balance)';
}
document.forms['bonus-other'].elements['user'].value = info.username;
purchase.disabled = false;
}
}

View File

@@ -12,9 +12,13 @@ if (isset($_POST['label'], $_POST['title'])) {
exit;
}
if (isset($_POST['bonus-user-other'])) {
if (isset($_POST['bonus-user-other'], $_POST['label'])) {
authorize(ajax: true);
echo new Json\BonusUserOther($_POST['bonus-user-other'])->response();
$item = new Manager\Bonus()->findBonusItemByLabel($_REQUEST['label']);
if (is_null($item)) {
json_error('bad label for other token item');
}
echo new Json\BonusUserOther($Viewer, $item, $_POST['bonus-user-other'])->response();
exit;
}

View File

@@ -1,7 +1,7 @@
{{ header("Bonus Points - Gift Tokens ", {'js': 'bonus'}) }}
<div class="thin">
<div class="header">
<h2><a href="bonus.php">Shop</a> Gift Tokens - {{ item.price | number_format }} Points</a></h2>
<h2><a href="bonus.php">Shop</a> Gift Tokens</a></h2>
</div>
<form name="bonus-other" action="bonus.php" method="post">
<div class="pad">

View File

@@ -60,6 +60,9 @@ class BonusTest extends TestCase {
);
$manager = new Manager\Bonus();
// Here is as good a place as any
$this->assertEquals(0, $manager->discount(), 'bonus-discount');
$manager->flush();
$this->assertCount(13, $manager->itemList(), 'bonus-item-list');
$this->assertNull($manager->findBonusItemByLabel('nope'), 'bonus-item-null');
@@ -127,49 +130,6 @@ class BonusTest extends TestCase {
'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)($other1->price() + $other50->price())
);
$this->assertEquals(
BonusItemPurchaseStatus::incomplete,
$other50->purchase(
$user,
$other50->price(),
),
'bonus-item-purchase-incomplete-other-50',
);
$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
$fileCount = $manager->findBonusItemByLabel('file-count');
$giver->addPoints((float)$fileCount->price());
@@ -187,33 +147,12 @@ class BonusTest extends TestCase {
$this->assertEquals(
$flt->price()
+ $other50->price()
+ $seedbox->price()
+ $fileCount->price(),
$giver->pointsSpent(),
'bonus-points-spent',
);
$this->assertEquals(
BonusItemPurchaseStatus::success,
$other1->purchase(
$this->userList['receiver'],
0,
['receiver' => $user],
),
'bonus-item-receiver-give'
);
$this->assertEquals(
[
"received" => 50,
"sent" => 1,
],
(new User\Bonus($this->userList['receiver'])->tokenExchange()),
'bonus-token-exchange',
);
$latest = $giver->otherLatest($this->userList['receiver']);
$this->assertEquals('50 Freeleech Tokens to Other', $latest['title'], 'bonus-item-given');
$bbn = $manager->findBonusItemByLabel('title-bb-n');
$giver->addPoints($bbn->price());
$this->assertEquals(
@@ -267,13 +206,12 @@ class BonusTest extends TestCase {
);
$history = $giver->history(10, 0);
$this->assertCount(8, $history, 'bonus-history-final');
$this->assertCount(7, $history, 'bonus-history-final');
$this->assertEquals(
[
'nr' => 8,
'nr' => 7,
'total' => $flt->price()
+ $other50->price()
+ $seedbox->price()
+ $fileCount->price()
+ $collage->price()
@@ -284,8 +222,16 @@ class BonusTest extends TestCase {
$giver->summary(),
'bonus-summary-initial'
);
$this->assertTrue($giver->removePoints(1.125), 'bonus-taketh-away');
$history = $giver->purchaseHistory();
$this->assertCount(7, $history, 'bonus-history-count');
$this->assertEquals(
['id', 'title', 'total', 'cost'],
array_keys(current($history)),
'bonus-history-shape',
);
$this->assertTrue($giver->removePoints(1.125), 'bonus-taketh-away');
$this->assertEquals(
BonusItemPurchaseStatus::insufficientFunds,
$bby->purchase($user, $bby->price(), ['title' => 'whatever']),
@@ -304,18 +250,157 @@ class BonusTest extends TestCase {
'bonus-item-free-title-yes-bb',
);
$history = $giver->purchaseHistory();
$this->assertCount(8, $history, 'bonus-history-count');
$this->assertEquals(
['id', 'title', 'total', 'cost'],
array_keys(current($history)),
'bonus-history-shape',
);
$this->assertCount(0, $giver->seedList(5, 0), 'bonus-history-seedlist');
$this->assertCount(0, $giver->poolHistory(), 'bonus-history-pool');
}
// Here is as good a place as any
$this->assertEquals(0, $manager->discount(), 'bonus-discount');
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();
$other1 = $manager->findBonusItemByLabel('other-1');
$other50 = $manager->findBonusItemByLabel('other-3');
$user = $this->userList['giver'];
$receiver = $this->userList['receiver'];
$giver = new User\Bonus($user);
$this->assertTrue($other1->needsPreparation(), 'bonus-token-other-prepare');
$this->assertEquals(
BonusItemPurchaseStatus::incomplete,
$other50->purchase(
$user,
$other50->price(),
),
'bonus-item-purchase-incomplete-other-50',
);
$giver->addPoints((float)$other50->price());
$receiver->toggleAttr('no-fl-gifts', true);
$this->assertEquals(
BonusItemPurchaseStatus::declined,
$other50->purchase(
$user,
$other50->price(),
[
'message' => 'phpunit gift',
'receiver' => $receiver,
],
),
'bonus-item-purchase-declined-other-50',
);
$receiver->toggleAttr('no-fl-gifts', false);
$this->assertEquals(
BonusItemPurchaseStatus::success,
$other50->purchase(
$user,
$other50->price(),
[
'message' => 'phpunit gift',
'receiver' => $receiver,
],
),
'bonus-item-purchase-other-50',
);
$this->assertEquals(
BonusItemPurchaseStatus::insufficientFunds,
$manager->purchaseTokenOther(
$user,
$receiver,
'other-1',
'',
),
'bonus-purchase-token-other-no-money',
);
$this->assertCount(0,
$manager->offerTokenOther($user),
'bonus-offer-other-none',
);
$giver->addPoints((float)$other1->price());
$offer = $manager->offerTokenOther($user);
$this->assertEquals('other-1', $offer[0]->label(), 'bonus-item-all-I-can-give');
$this->assertEquals(
BonusItemPurchaseStatus::incomplete,
$manager->purchaseTokenOther(
$user,
$receiver,
'bad-label',
'',
),
'bonus-purchase-token-other-fail',
);
$this->assertEquals(
BonusItemPurchaseStatus::success,
$manager->purchaseTokenOther(
$user,
$receiver,
'other-1',
'',
),
'bonus-purchase-token-other-success',
);
$this->assertEquals(
BonusItemPurchaseStatus::success,
$other1->purchase(
$receiver,
0,
['receiver' => $user],
),
'bonus-item-receiver-give'
);
$this->assertEquals(
[
"received" => 51,
"sent" => 1,
],
(new User\Bonus($receiver)->tokenExchange()),
'bonus-token-exchange',
);
$latest = $giver->otherLatest($receiver);
$this->assertEquals(
'50 Freeleech Tokens to Other',
$latest['title'],
'bonus-item-given'
);
$giver->addPoints((float)$other50->price() * 2);
$this->assertCount(
3,
$manager->offerTokenOther($user),
'bonus-offer-other-all',
);
$this->assertFalse(
$manager->findBonusItemByLabel('invite')
->priceForTokenOther($user, $receiver),
'bonus-price-other-false',
);
$basePrice = $other50->priceForTokenOther($user, $receiver);
$this->assertNotFalse($basePrice, 'bonus-price-token-other-valid');
$other50->purchase(
$user,
$other50->price(),
[
'message' => 'phpunit gift 2',
'receiver' => $receiver,
],
);
$this->assertEquals(
$basePrice * (1 + BONUS_OTHER_TOKEN_SCALE / 100),
$other50->priceForTokenOther($user, $receiver),
'bonus-price-other-increase',
);
$receiver->setField('inviter_user_id', $user->id)->modify();
$this->assertEquals(
$basePrice,
$other50->priceForTokenOther($user, $receiver),
'bonus-price-other-inviter',
);
}
public static function providerBonusItem(): array {
@@ -342,7 +427,11 @@ class BonusTest extends TestCase {
$this->userList[] = Helper::makeUser('bonusg.' . randomString(6), 'bonus', true);
$this->assertEquals(
BonusItemPurchaseStatus::insufficientFunds,
$item->purchase($this->userList[0], $item->price(), ['receiver' => $this->userList[1]]),
$item->purchase(
$this->userList[0],
$item->price(),
['receiver' => $this->userList[1]]
),
"bonus-item-broke-$label",
);
} else {
@@ -354,61 +443,12 @@ class BonusTest extends TestCase {
}
}
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 {
global $Cache;
$Cache->delete_value("bonus_pool");
$manager = new Manager\Bonus();
$this->assertEquals(
[],
$manager->openPoolList(),
new Manager\Bonus()->openPoolList(),
'bonus-open-pool',
);
}
@@ -453,50 +493,58 @@ class BonusTest extends TestCase {
}
public function testBonusUserOther(): void {
$user = Helper::makeUser('bonusother.' . randomString(6), 'bonus', enable: false);
$this->userList = [$user];
$giver = Helper::makeUser('bonusother.' . randomString(6), 'bonus', enable: false);
$receiver = Helper::makeUser('bonusother.' . randomString(6), 'bonus', enable: false);
$this->userList = [$giver, $receiver];
$token = new Manager\Bonus()->findBonusItemByLabel('other-1');
$this->assertEquals(
[
'found' => false,
'username' => '#nope',
],
new Json\BonusUserOther('#nope')->payload(),
new Json\BonusUserOther($giver, $token, '#nope')->payload(),
'bonus-user-other-404',
);
$this->assertEquals(
$this->assertEqualsCanonicalizing(
[
'found' => true,
'accept' => true,
'enabled' => false,
'id' => $user->id,
'username' => $user->username(),
'found' => true,
'id' => $receiver->id,
'percent5' => 0,
'price' => 2500,
'username' => $receiver->username(),
],
new Json\BonusUserOther($user->username())->payload(),
new Json\BonusUserOther($giver, $token, $receiver->username())->payload(),
'bonus-user-other-not-enabled',
);
$user->setField('Enabled', UserStatus::enabled->value)->modify();
$this->assertEquals(
$receiver->setField('Enabled', UserStatus::enabled->value)->modify();
$this->assertEqualsCanonicalizing(
[
'found' => true,
'accept' => true,
'enabled' => true,
'id' => $user->id,
'username' => $user->username(),
'found' => true,
'id' => $receiver->id,
'percent5' => 0,
'price' => 2500,
'username' => $receiver->username(),
],
new Json\BonusUserOther($user->username())->payload(),
new Json\BonusUserOther($giver, $token, $receiver->username())->payload(),
'bonus-user-other-accept',
);
$user->toggleAttr('no-fl-gifts', true);
$this->assertEquals(
$receiver->toggleAttr('no-fl-gifts', true);
$this->assertEqualsCanonicalizing(
[
'found' => true,
'accept' => false,
'enabled' => true,
'id' => $user->id,
'username' => $user->username(),
'found' => true,
'id' => $receiver->id,
'percent5' => 0,
'price' => 2500,
'username' => $receiver->username(),
],
new Json\BonusUserOther($user->username())->payload(),
new Json\BonusUserOther($giver, $token, $receiver->username())->payload(),
'bonus-user-other-no-fl',
);
}