Commit 130b7876 authored by Emiliano Balbuena's avatar Emiliano Balbuena

(feat): Refunds

1 merge request!235WIP: Boost Campaigns (&24)
Pipeline #71165737 passed with stages
in 6 minutes and 57 seconds
......@@ -48,6 +48,9 @@ class views implements Interfaces\Api
->setCampaign($campaign)
->increment();
$campaignsManager
->onImpression($campaign);
// NOTE: Campaigns have a _single_ entity, for now. Refactor this when we support multiple
// Ideally, we should use a composite URN, like: urn:campaign-entity:100000321:(urn:activity:100000500)
foreach ($campaign->getEntityUrns() as $entityUrn) {
......
......@@ -169,17 +169,21 @@ class Manager
/**
* Adds a transaction to the queue
* @param $transaction
* @param bool $handleEvents
*/
public function add($transaction)
public function add($transaction, $handleEvents = true)
{
$this->repo->add($transaction);
$this->queue->setQueue("BlockchainTransactions")
->send([
'user_guid' => $transaction->getUserGuid(),
'timestamp' => $transaction->getTimestamp(),
'wallet_address' => $transaction->getWalletAddress(),
'tx' => $transaction->getTx(),
]);
if ($handleEvents) {
$this->queue->setQueue("BlockchainTransactions")
->send([
'user_guid' => $transaction->getUserGuid(),
'timestamp' => $transaction->getTimestamp(),
'wallet_address' => $transaction->getWalletAddress(),
'tx' => $transaction->getTx(),
]);
}
}
/**
......
......@@ -9,6 +9,7 @@ namespace Minds\Core\Boost\Campaigns\Delegates;
use Exception;
use Minds\Core\Boost\Campaigns\Campaign;
use Minds\Core\Boost\Campaigns\CampaignException;
use Minds\Core\Boost\Campaigns\Metrics;
use Minds\Core\Boost\Campaigns\Payments;
use Minds\Core\Boost\Campaigns\Payments\Payment;
use Minds\Core\Config;
......@@ -22,18 +23,25 @@ class PaymentsDelegate
/** @var Payments\Onchain */
protected $onchainPayments;
/** @var Metrics */
protected $metrics;
/**
* PaymentsDelegate constructor.
* @param Config $config
* @param Payments\Onchain $onchainPayments
* @param Metrics $metrics
* @throws Exception
*/
public function __construct(
$config = null,
$onchainPayments = null
$onchainPayments = null,
$metrics = null
)
{
$this->config = $config ?: Di::_()->get('Config');
$this->onchainPayments = $onchainPayments ?: new Payments\Onchain();
$this->metrics = $metrics ?: new Metrics();
}
/**
......@@ -47,7 +55,14 @@ class PaymentsDelegate
// NOTE: Do not spec test. Individually test the other methods.
$this->validateBudget($campaign);
$this->registerCampaignPayment($campaign, $paymentPayload);
if (!$paymentPayload) {
throw new CampaignException('Missing payment');
}
$this->pay($campaign, $paymentPayload);
$this->validatePayments($campaign);
$campaign = $this->updateImpressionsByCpm($campaign);
......@@ -69,7 +84,12 @@ class PaymentsDelegate
->setBudgetType($campaign->getBudgetType());
$this->validateBudget($campaignRef);
$this->registerCampaignPayment($campaignRef, $paymentPayload);
if ($paymentPayload) {
// TODO: This looks wrong, we should act upon campaign, not campaignRef
$this->pay($campaignRef, $paymentPayload);
$this->validatePayments($campaignRef);
}
$campaign = $this->updateImpressionsByCpm($campaign);
......@@ -79,14 +99,21 @@ class PaymentsDelegate
/**
* @param Campaign $campaign
* @return Campaign
* @throws CampaignException
*/
public function onStateChange(Campaign $campaign)
{
// NOTE: Do not spec test. Individually test the other methods.
// TODO: Check that campaign is in a final complete or incomplete status (revoked/rejected)
// TODO: Refund!
// TODO: Store refund info onto Campaign/Boost metadata
$isFinished = in_array($campaign->getDeliveryStatus(), [
Campaign::REJECTED_STATUS,
Campaign::REVOKED_STATUS,
Campaign::COMPLETED_STATUS
]);
if ($isFinished) {
$this->refund($campaign);
}
return $campaign;
}
......@@ -109,18 +136,25 @@ class PaymentsDelegate
return $campaign;
}
/**
* @param Campaign $campaign
* @return Campaign
*/
public function validatePayments(Campaign $campaign)
{
// TODO: Validate all payments
return $campaign;
}
/**
* @param Campaign $campaign
* @param mixed $payload
* @return Campaign
* @throws CampaignException
*/
public function registerCampaignPayment(Campaign $campaign, $payload)
public function pay(Campaign $campaign, $payload)
{
if (!$payload) {
return $campaign;
}
switch ($campaign->getBudgetType()) {
case 'tokens':
if (!$payload || !$payload['txHash'] || !$payload['address'] || !$payload['amount']) {
......@@ -133,7 +167,7 @@ class PaymentsDelegate
->setCampaignGuid($campaign->getGuid())
->setTx($payload['txHash'])
->setSource($payload['address'])
->setAmount($payload['amount'])
->setAmount((double) $payload['amount'])
->setTimeCreated(time());
try {
......@@ -153,6 +187,86 @@ class PaymentsDelegate
return $campaign;
}
/**
* @param Campaign $campaign
* @return Campaign
* @throws CampaignException
* @throws Exception
*/
public function refund(Campaign $campaign)
{
$latestPaymentSource = '';
// Sum up all payments
$paid = 0;
foreach ($campaign->getPayments() as $payment) {
// Sum!
$paid += $payment->getAmount();
if ($payment->getAmount() > 0 && $payment->getSource()) {
// Grab the latest wallet used by campaign's owner
$latestPaymentSource = $payment->getSource();
}
}
// If amount is < 0.1 (minimum fraction), don't refund
if ($paid <= 0.1) {
return $campaign;
}
// Grab a fresh count of impressions met
$impressionsMet = $this->metrics
->setCampaign($campaign)
->get();
// Calculate the cost of the impressions met
$cost = ($impressionsMet / 1000) * $campaign->cpm();
// Calculate the amount to be refunded
$amount = $paid - $cost;
// If amount is < 0.1 (minimum fraction), don't refund
if ($amount < 0.1) {
return $campaign;
}
// Execute refund
switch ($campaign->getBudgetType()) {
case 'tokens':
$payment = new Payment();
$payment
->setOwnerGuid($campaign->getOwnerGuid())
->setCampaignGuid($campaign->getGuid())
->setSource($latestPaymentSource)
->setAmount(-$amount)
->setTimeCreated(time());
try {
$this->onchainPayments
->refund($payment);
} catch (Exception $e) {
throw new CampaignException("Error registering refund: {$e->getMessage()}");
}
$campaign->pushPayment($payment);
break;
default:
throw new CampaignException('Unknown budget type');
}
return $campaign;
}
/**
* @param Campaign $campaign
* @param Payment $paymentRef
......
......@@ -7,16 +7,12 @@
namespace Minds\Core\Boost\Campaigns;
use Exception;
use Minds\Common\Urn;
use Minds\Core\Counters\Manager as Counters;
use Minds\Core\Di\Di;
use Minds\Core\Entities\Resolver;
class Metrics
{
/** @var Manager */
protected $manager;
/** @var Counters */
protected $counters;
......@@ -28,35 +24,27 @@ class Metrics
/**
* Metrics constructor.
* @param Manager $manager
* @param Counters $counters
* @param Resolver $resolver
* @throws Exception
*/
public function __construct(
$manager = null,
$counters = null,
$resolver = null
)
{
$this->manager = $manager ?: new Manager();
$this->counters = $counters ?: Di::_()->get('Counters');
$this->resolver = $resolver ?: new Resolver();
}
/**
* @param Campaign|Urn|string $campaign
* @param Campaign $campaign
* @return Metrics
* @throws Exception
*/
public function setCampaign($campaign)
public function setCampaign(Campaign $campaign)
{
if (is_object($campaign) && $campaign instanceof Campaign) {
$this->campaign = $campaign;
} else {
$this->campaign = $this->manager->get((string) $campaign);
}
$this->campaign = $campaign;
return $this;
}
......@@ -80,10 +68,6 @@ class Metrics
->setMetric('boost_impressions')
->increment();
// Pass down impressions update to manager
$this->manager->onImpression($this->campaign);
// Increment entity counters
// NOTE: Campaigns have a _single_ entity, for now. Refactor this when we support multiple
// Ideally, we should use a composite URN, like: urn:campaign-entity:100000321:(urn:activity:100000500)
......@@ -105,15 +89,24 @@ class Metrics
}
/**
* @return Campaign
* @return int
* @throws Exception
*/
public function syncImpressionsMet()
public function get()
{
$count = $this->counters
return $this->counters
->setEntityGuid($this->campaign->getGuid())
->setMetric('boost_impressions')
->get(false);
}
/**
* @return Campaign
* @throws Exception
*/
public function syncImpressionsMet()
{
$count = $this->get();
$this->campaign
->setImpressionsMet($count);
......
......@@ -10,7 +10,6 @@ use Exception;
use Minds\Core\Blockchain\Services\Ethereum;
use Minds\Core\Blockchain\Transactions\Manager as TransactionsManager;
use Minds\Core\Blockchain\Transactions\Transaction;
use Minds\Core\Boost\Campaigns\Campaign;
use Minds\Core\Config;
use Minds\Core\Di\Di;
use Minds\Core\Util\BigNumber;
......@@ -57,7 +56,7 @@ class Onchain
$transaction
->setTx($payment->getTx())
->setContract('boost_campaign')
->setAmount((string) BigNumber::toPlain($payment->getAmount(), 18))
->setAmount((string) BigNumber::toPlain($payment->getAmount(), 18)->neg())
->setWalletAddress($payment->getSource())
->setTimestamp($payment->getTimeCreated())
->setUserGuid($payment->getOwnerGuid())
......@@ -70,4 +69,59 @@ class Onchain
return true;
}
/**
* @param Payment $payment
* @return Payment
* @throws Exception
*/
public function refund(Payment $payment)
{
$token = $this->config->get('blockchain')['token_address'];
$wallet = $this->config->get('blockchain')['contracts']['boost_campaigns']['wallet_address'] ?? null;
$walletKey = $this->config->get('blockchain')['contracts']['boost_campaigns']['wallet_pkey'] ?? null;
if (!$token) {
throw new Exception('Invalid token contract address used for refund');
} elseif (!$wallet || !$walletKey) {
throw new Exception('Invalid Boost Campaigns wallet address used as refund source');
} elseif (!$payment->getSource()) {
throw new Exception('Invalid Boost Campaign refund destination wallet address');
} elseif ($payment->getAmount() > 0) {
throw new Exception('Refunds can only happen on negative payment amounts');
}
$txWeiAmount = BigNumber::toPlain($payment->getAmount(), 18)->neg();
$refundTx = $this->eth->sendRawTransaction($walletKey, [
'from' => $wallet,
'to' => $token,
'gasLimit' => BigNumber::_(4612388)->toHex(true),
'gasPrice' => BigNumber::_(10000000000)->toHex(true),
'data' => $this->eth->encodeContractMethod('transfer(address,uint256)', [
$payment->getSource(),
$txWeiAmount->toHex(true),
]),
]);
$payment
->setTx($refundTx);
$transaction = new Transaction();
$transaction
->setTx($payment->getTx())
->setContract('boost_campaign')
->setAmount((string) $txWeiAmount)
->setWalletAddress($payment->getSource())
->setTimestamp($payment->getTimeCreated())
->setUserGuid($payment->getOwnerGuid())
->setData([
'payment' => $payment->export(),
]);
$this->repository->add($payment);
$this->txManager->add($transaction, false);
return $payment;
}
}
......@@ -20,8 +20,8 @@ use Minds\Traits\MagicAttributes;
* @method Payment setTx(string $tx)
* @method string getSource()
* @method Payment setSource(string $source)
* @method string getAmount()
* @method Payment setAmount(string $amount)
* @method double getAmount()
* @method Payment setAmount(double $amount)
* @method int getTimeCreated()
* @method Payment setTimeCreated(int $timeCreated)
*/
......
......@@ -66,6 +66,7 @@ class Minds extends base
(new Config\ConfigProvider())->register();
(new OAuth\OAuthProvider())->register();
(new Sessions\SessionsProvider())->register();
(new Counters\CountersProvider())->register();
(new Boost\BoostProvider())->register();
(new Data\DataProvider())->register();
//(new Core\Notification\NotificationProvider())->register();
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment