diff --git a/src/app/Console/Commands/WalletTransactions.php b/src/app/Console/Commands/WalletTransactions.php --- a/src/app/Console/Commands/WalletTransactions.php +++ b/src/app/Console/Commands/WalletTransactions.php @@ -60,9 +60,8 @@ foreach ($elements as $element) { $this->info( sprintf( - " + %s: %s %s", + " + %s: %s", $element->id, - $element->created_at, $element->toString() ) ); diff --git a/src/app/Entitlement.php b/src/app/Entitlement.php --- a/src/app/Entitlement.php +++ b/src/app/Entitlement.php @@ -85,7 +85,6 @@ { $transaction = \App\Transaction::create( [ - 'user_email' => \App\Utils::userEmailOrNull(), 'object_id' => $this->id, 'object_type' => \App\Entitlement::class, 'type' => $type, @@ -107,6 +106,22 @@ } /** + * Returns entitleable object title (e.g. email or domain name). + * + * @return string|null An object title/name + */ + public function entitleableTitle(): ?string + { + if ($this->entitleable instanceof \App\User) { + return $this->entitleable->email; + } + + if ($this->entitleable instanceof \App\Domain) { + return $this->entitleable->namespace; + } + } + + /** * The SKU concerned. * * @return \Illuminate\Database\Eloquent\Relations\BelongsTo diff --git a/src/app/Observers/TransactionObserver.php b/src/app/Observers/TransactionObserver.php --- a/src/app/Observers/TransactionObserver.php +++ b/src/app/Observers/TransactionObserver.php @@ -22,5 +22,9 @@ break; } } + + if (!isset($transaction->user_email)) { + $transaction->user_email = \App\Utils::userEmailOrNull(); + } } } diff --git a/src/app/Transaction.php b/src/app/Transaction.php --- a/src/app/Transaction.php +++ b/src/app/Transaction.php @@ -2,6 +2,8 @@ namespace App; +use App\Entitlement; +use App\Wallet; use Illuminate\Database\Eloquent\Model; /** @@ -18,6 +20,15 @@ */ class Transaction extends Model { + public const ENTITLEMENT_BILLED = 'billed'; + public const ENTITLEMENT_CREATED = 'created'; + public const ENTITLEMENT_DELETED = 'deleted'; + + public const WALLET_AWARD = 'award'; + public const WALLET_CREDIT = 'credit'; + public const WALLET_DEBIT = 'debit'; + public const WALLET_PENALTY = 'penalty'; + protected $fillable = [ // actor, if any 'user_email', @@ -49,25 +60,27 @@ /** @var string The type of the primary key */ protected $keyType = 'string'; - public const ENTITLEMENT_BILLED = 'billed'; - public const ENTITLEMENT_CREATED = 'created'; - public const ENTITLEMENT_DELETED = 'deleted'; - public const WALLET_AWARD = 'award'; - public const WALLET_CREDIT = 'credit'; - public const WALLET_DEBIT = 'debit'; - public const WALLET_PENALTY = 'penalty'; - - public function entitlement() + /** + * Returns the entitlement to which the transaction is assigned (if any) + * + * @return \App\Entitlement|null The entitlement + */ + public function entitlement(): ?Entitlement { - if ($this->object_type !== \App\Entitlement::class) { + if ($this->object_type !== Entitlement::class) { return null; } - return \App\Entitlement::withTrashed()->where('id', $this->object_id)->first(); + return Entitlement::withTrashed()->find($this->object_id); } - public function setTypeAttribute($value) + /** + * Transaction type mutator + * + * @throws \Exception + */ + public function setTypeAttribute($value): void { switch ($value) { case self::ENTITLEMENT_BILLED: @@ -90,120 +103,72 @@ } } - public function toArray() - { - $result = [ - 'user_email' => $this->user_email, - 'entitlement_cost' => $this->getEntitlementCost(), - 'object_email' => $this->getEntitlementObjectEmail(), - 'sku_title' => $this->getEntitlementSkuTitle(), - 'wallet_description' => $this->getWalletDescription(), - 'description' => $this->{'description'}, - 'amount' => $this->amount - ]; - - return $result; - } - - public function toString() - { - $label = $this->objectTypeToLabelString() . '-' . $this->{'type'}; - - return \trans("transactions.{$label}", $this->toArray()); - } - - public function shortDescription() - { - $label = $this->objectTypeToLabelString() . '-' . $this->{'type'} . '-short'; - - return \trans("transactions.{$label}", $this->toArray()); - } - - public function wallet() - { - if ($this->object_type !== \App\Wallet::class) { - return null; - } - - return \App\Wallet::where('id', $this->object_id)->first(); - } - /** - * Return the costs for this entitlement. + * Returns a short text describing the transaction. * - * @return int|null + * @return string The description */ - private function getEntitlementCost(): ?int + public function shortDescription(): string { - if (!$this->entitlement()) { - return null; - } - - // FIXME: without wallet discount - // FIXME: in cents - // FIXME: without wallet currency - $cost = $this->entitlement()->cost; - - $discount = $this->entitlement()->wallet->getDiscountRate(); + $label = $this->objectTypeToLabelString() . '-' . $this->{'type'} . '-short'; - return $cost * $discount; + return \trans("transactions.{$label}", $this->descriptionParams()); } /** - * Return the object email if any. This is the email for the target user entitlement. + * Returns a text describing the transaction. * - * @return string|null + * @return string The description */ - private function getEntitlementObjectEmail(): ?string + public function toString(): string { - $entitlement = $this->entitlement(); - - if (!$entitlement) { - return null; - } - - $user = \App\User::withTrashed()->where('id', $entitlement->object_id)->first(); - - if (!$user) { - \Log::debug("No entitleable for {$entitlement->id} ?"); - return null; - } + $label = $this->objectTypeToLabelString() . '-' . $this->{'type'}; - return $user->email; + return \trans("transactions.{$label}", $this->descriptionParams()); } /** - * Return the title for the SKU this entitlement is for. + * Returns a wallet to which the transaction is assigned (if any) * - * @return string|null + * @return \App\Wallet|null The wallet */ - private function getEntitlementSkuTitle(): ?string + public function wallet(): ?Wallet { - if (!$this->entitlement()) { + if ($this->object_type !== Wallet::class) { return null; } - return $this->entitlement()->sku->{'title'}; + return Wallet::find($this->object_id); } /** - * Return the description for the wallet, if any, or 'default wallet'. + * Collect transaction parameters used in (localized) descriptions * - * @return string + * @return array Parameters */ - public function getWalletDescription() + private function descriptionParams(): array { - $description = null; + $result = [ + 'user_email' => $this->user_email, + 'description' => $this->{'description'}, + ]; if ($entitlement = $this->entitlement()) { - $description = $entitlement->wallet->{'description'}; + $wallet = $entitlement->wallet; + $cost = $entitlement->cost; + $discount = $entitlement->wallet->getDiscountRate(); + + $result['entitlement_cost'] = $cost * $discount; + $result['object'] = $entitlement->entitleableTitle(); + $result['sku_title'] = $entitlement->sku->{'title'}; + } else { + $wallet = $this->wallet(); } - if ($wallet = $this->wallet()) { - $description = $wallet->{'description'}; - } + $result['wallet'] = $wallet->{'description'} ?: 'Default wallet'; + $result['amount'] = $wallet->money($this->amount); - return $description ?: 'Default wallet'; + return $result; } /** @@ -213,11 +178,11 @@ */ private function objectTypeToLabelString(): ?string { - if ($this->object_type == \App\Entitlement::class) { + if ($this->object_type == Entitlement::class) { return 'entitlement'; } - if ($this->object_type == \App\Wallet::class) { + if ($this->object_type == Wallet::class) { return 'wallet'; } diff --git a/src/app/Wallet.php b/src/app/Wallet.php --- a/src/app/Wallet.php +++ b/src/app/Wallet.php @@ -211,7 +211,6 @@ \App\Transaction::create( [ - 'user_email' => \App\Utils::userEmailOrNull(), 'object_id' => $this->id, 'object_type' => \App\Wallet::class, 'type' => \App\Transaction::WALLET_CREDIT, @@ -243,7 +242,6 @@ $transaction = \App\Transaction::create( [ - 'user_email' => \App\Utils::userEmailOrNull(), 'object_id' => $this->id, 'object_type' => \App\Wallet::class, 'type' => \App\Transaction::WALLET_DEBIT, diff --git a/src/resources/lang/en/transactions.php b/src/resources/lang/en/transactions.php --- a/src/resources/lang/en/transactions.php +++ b/src/resources/lang/en/transactions.php @@ -1,18 +1,18 @@ ':user_email created :sku_title for :object_email', - 'entitlement-billed' => ':sku_title for :object_email is billed at :amount', - 'entitlement-deleted' => ':user_email deleted :sku_title for :object_email', + 'entitlement-created' => ':user_email created :sku_title for :object', + 'entitlement-billed' => ':sku_title for :object is billed at :amount', + 'entitlement-deleted' => ':user_email deleted :sku_title for :object', - 'wallet-award' => 'Bonus of :amount awarded to :wallet_description; :description', - 'wallet-credit' => ':amount was added to the balance of :wallet_description', - 'wallet-debit' => ':amount was deducted from the balance of :wallet_description', - 'wallet-penalty' => 'The balance of wallet :wallet_description was reduced by :amount; :description', + 'wallet-award' => 'Bonus of :amount awarded to :wallet; :description', + 'wallet-credit' => ':amount was added to the balance of :wallet', + 'wallet-debit' => ':amount was deducted from the balance of :wallet', + 'wallet-penalty' => 'The balance of :wallet was reduced by :amount; :description', - 'entitlement-created-short' => 'Added :sku_title for :object_email', - 'entitlement-billed-short' => 'Billed :sku_title for :object_email', - 'entitlement-deleted-short' => 'Deleted :sku_title for :object_email', + 'entitlement-created-short' => 'Added :sku_title for :object', + 'entitlement-billed-short' => 'Billed :sku_title for :object', + 'entitlement-deleted-short' => 'Deleted :sku_title for :object', 'wallet-award-short' => 'Bonus: :description', 'wallet-credit-short' => 'Payment', diff --git a/src/tests/Unit/TransactionTest.php b/src/tests/Unit/TransactionTest.php --- a/src/tests/Unit/TransactionTest.php +++ b/src/tests/Unit/TransactionTest.php @@ -2,58 +2,180 @@ namespace Tests\Unit; +use App\Entitlement; +use App\Sku; use App\Transaction; +use App\Wallet; use Tests\TestCase; class TransactionTest extends TestCase { - public function setUp(): void + /** + * Test transaction short and long labels + */ + public function testLabels(): void { - parent::setUp(); - } + // Prepare test environment + Transaction::where('amount', '<', 20)->delete(); + $user = $this->getTestUser('jane@kolabnow.com'); + $wallet = $user->wallets()->first(); - public function tearDown(): void - { - parent::tearDown(); - } + // Create transactions - public function testLabel() - { - $transactions = Transaction::limit(20)->get(); + $transaction = Transaction::create([ + 'object_id' => $wallet->id, + 'object_type' => Wallet::class, + 'type' => Transaction::WALLET_PENALTY, + 'amount' => 9, + 'description' => "A test penalty" + ]); - foreach ($transactions as $transaction) { - $this->assertNotNull($transaction->toString()); - } - } + $transaction = Transaction::create([ + 'object_id' => $wallet->id, + 'object_type' => Wallet::class, + 'type' => Transaction::WALLET_DEBIT, + 'amount' => 10 + ]); - public function testWalletPenalty() - { - $user = $this->getTestUser('jane@kolabnow.com'); - $wallet = $user->wallets()->first(); + $transaction = Transaction::create([ + 'object_id' => $wallet->id, + 'object_type' => Wallet::class, + 'type' => Transaction::WALLET_CREDIT, + 'amount' => 11 + ]); - $transaction = Transaction::create( - [ + $transaction = Transaction::create([ 'object_id' => $wallet->id, - 'object_type' => \App\Wallet::class, - 'type' => Transaction::WALLET_PENALTY, - 'amount' => 9 - ] + 'object_type' => Wallet::class, + 'type' => Transaction::WALLET_AWARD, + 'amount' => 12, + 'description' => "A test award" + ]); + + $sku = Sku::where('title', 'mailbox')->first(); + $entitlement = Entitlement::where('sku_id', $sku->id)->first(); + $transaction = Transaction::create([ + 'user_email' => 'test@test.com', + 'object_id' => $entitlement->id, + 'object_type' => Entitlement::class, + 'type' => Transaction::ENTITLEMENT_CREATED, + 'amount' => 13 + ]); + + $sku = Sku::where('title', 'domain-hosting')->first(); + $entitlement = Entitlement::where('sku_id', $sku->id)->first(); + $transaction = Transaction::create([ + 'user_email' => 'test@test.com', + 'object_id' => $entitlement->id, + 'object_type' => Entitlement::class, + 'type' => Transaction::ENTITLEMENT_BILLED, + 'amount' => 14 + ]); + + $sku = Sku::where('title', 'storage')->first(); + $entitlement = Entitlement::where('sku_id', $sku->id)->first(); + $transaction = Transaction::create([ + 'user_email' => 'test@test.com', + 'object_id' => $entitlement->id, + 'object_type' => Entitlement::class, + 'type' => Transaction::ENTITLEMENT_DELETED, + 'amount' => 15 + ]); + + $transactions = Transaction::where('amount', '<', 20)->orderBy('amount')->get(); + + $this->assertSame(9, $transactions[0]->amount); + $this->assertSame(Transaction::WALLET_PENALTY, $transactions[0]->type); + $this->assertSame( + "The balance of Default wallet was reduced by 0,09 CHF; A test penalty", + $transactions[0]->toString() + ); + $this->assertSame( + "Charge: A test penalty", + $transactions[0]->shortDescription() ); - $this->assertEquals($transaction->{'type'}, Transaction::WALLET_PENALTY); + $this->assertSame(10, $transactions[1]->amount); + $this->assertSame(Transaction::WALLET_DEBIT, $transactions[1]->type); + $this->assertSame( + "0,10 CHF was deducted from the balance of Default wallet", + $transactions[1]->toString() + ); + $this->assertSame( + "Deduction", + $transactions[1]->shortDescription() + ); + + $this->assertSame(11, $transactions[2]->amount); + $this->assertSame(Transaction::WALLET_CREDIT, $transactions[2]->type); + $this->assertSame( + "0,11 CHF was added to the balance of Default wallet", + $transactions[2]->toString() + ); + $this->assertSame( + "Payment", + $transactions[2]->shortDescription() + ); + + $this->assertSame(12, $transactions[3]->amount); + $this->assertSame(Transaction::WALLET_AWARD, $transactions[3]->type); + $this->assertSame( + "Bonus of 0,12 CHF awarded to Default wallet; A test award", + $transactions[3]->toString() + ); + $this->assertSame( + "Bonus: A test award", + $transactions[3]->shortDescription() + ); + + $ent = $transactions[4]->entitlement(); + $this->assertSame(13, $transactions[4]->amount); + $this->assertSame(Transaction::ENTITLEMENT_CREATED, $transactions[4]->type); + $this->assertSame( + "test@test.com created mailbox for " . $ent->entitleableTitle(), + $transactions[4]->toString() + ); + $this->assertSame( + "Added mailbox for " . $ent->entitleableTitle(), + $transactions[4]->shortDescription() + ); + + $ent = $transactions[5]->entitlement(); + $this->assertSame(14, $transactions[5]->amount); + $this->assertSame(Transaction::ENTITLEMENT_BILLED, $transactions[5]->type); + $this->assertSame( + sprintf("%s for %s is billed at 0,14 CHF", $ent->sku->title, $ent->entitleableTitle()), + $transactions[5]->toString() + ); + $this->assertSame( + sprintf("Billed %s for %s", $ent->sku->title, $ent->entitleableTitle()), + $transactions[5]->shortDescription() + ); + + $ent = $transactions[6]->entitlement(); + $this->assertSame(15, $transactions[6]->amount); + $this->assertSame(Transaction::ENTITLEMENT_DELETED, $transactions[6]->type); + $this->assertSame( + sprintf("test@test.com deleted %s for %s", $ent->sku->title, $ent->entitleableTitle()), + $transactions[6]->toString() + ); + $this->assertSame( + sprintf("Deleted %s for %s", $ent->sku->title, $ent->entitleableTitle()), + $transactions[6]->shortDescription() + ); } - public function testInvalidType() + /** + * Test that an exception is being thrown on invalid type + */ + public function testInvalidType(): void { - $user = $this->getTestUser('jane@kolabnow.com'); - $wallet = $user->wallets()->first(); - $this->expectException(\Exception::class); $transaction = Transaction::create( [ - 'object_id' => $wallet->id, - 'object_type' => \App\Wallet::class, + 'object_id' => 'fake-id', + 'object_type' => Wallet::class, 'type' => 'invalid', 'amount' => 9 ]