diff --git a/docker/redis/Dockerfile b/docker/redis/Dockerfile --- a/docker/redis/Dockerfile +++ b/docker/redis/Dockerfile @@ -1,5 +1,6 @@ FROM fedora:30 +ENV container docker ENV SYSTEMD_PAGER='' RUN dnf -y install \ diff --git a/src/.gitignore b/src/.gitignore --- a/src/.gitignore +++ b/src/.gitignore @@ -1,3 +1,4 @@ +*.swp database/database.sqlite node_modules/ package-lock.json diff --git a/src/app/Console/Commands/WalletTransactions.php b/src/app/Console/Commands/WalletTransactions.php new file mode 100644 --- /dev/null +++ b/src/app/Console/Commands/WalletTransactions.php @@ -0,0 +1,73 @@ +argument('wallet'))->first(); + + if (!$wallet) { + return 1; + } + + foreach ($wallet->transactions() as $transaction) { + $this->info( + sprintf( + "%s: %s %s", + $transaction->id, + $transaction->created_at, + $transaction->toString() + ) + ); + + if ($this->option('detail')) { + $elements = \App\Transaction::where('transaction_id', $transaction->id) + ->orderBy('created_at')->get(); + + foreach ($elements as $element) { + $this->info( + sprintf( + " + %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 @@ -53,13 +53,37 @@ ]; /** + * Create a transaction record for this entitlement. + * + * @param string $type The type of transaction ('created', 'billed', 'deleted'), but use the + * \App\Transaction constants. + * @param int $amount The amount involved in cents + * + * @return string The transaction ID + */ + public function createTransaction($type, $amount = null) + { + $transaction = \App\Transaction::create( + [ + 'user_email' => \App\Utils::userEmailOrNull(), + 'object_id' => $this->id, + 'object_type' => \App\Entitlement::class, + 'type' => $type, + 'amount' => $amount + ] + ); + + return $transaction->id; + } + + /** * Principally entitleable objects such as 'Domain' or 'User'. * * @return mixed */ public function entitleable() { - return $this->morphTo(); + return $this->morphTo()->withTrashed(); } /** diff --git a/src/app/Http/Controllers/API/V4/Admin/WalletsController.php b/src/app/Http/Controllers/API/V4/Admin/WalletsController.php --- a/src/app/Http/Controllers/API/V4/Admin/WalletsController.php +++ b/src/app/Http/Controllers/API/V4/Admin/WalletsController.php @@ -42,7 +42,7 @@ $result['provider'] = $provider->name(); $result['providerLink'] = $provider->customerLink($wallet); - return $result; + return response()->json($result); } /** diff --git a/src/app/Http/Controllers/API/V4/WalletsController.php b/src/app/Http/Controllers/API/V4/WalletsController.php --- a/src/app/Http/Controllers/API/V4/WalletsController.php +++ b/src/app/Http/Controllers/API/V4/WalletsController.php @@ -45,7 +45,7 @@ /** * Display the specified resource. * - * @param int $id + * @param string $id * * @return \Illuminate\Http\JsonResponse */ diff --git a/src/app/Observers/EntitlementObserver.php b/src/app/Observers/EntitlementObserver.php --- a/src/app/Observers/EntitlementObserver.php +++ b/src/app/Observers/EntitlementObserver.php @@ -67,6 +67,8 @@ { $entitlement->entitleable->updated_at = Carbon::now(); $entitlement->entitleable->save(); + + $entitlement->createTransaction(\App\Transaction::ENTITLEMENT_CREATED); } /** @@ -87,5 +89,7 @@ $entitlement->entitleable->updated_at = Carbon::now(); $entitlement->entitleable->save(); + + $entitlement->createTransaction(\App\Transaction::ENTITLEMENT_DELETED); } } diff --git a/src/app/Observers/TransactionObserver.php b/src/app/Observers/TransactionObserver.php new file mode 100644 --- /dev/null +++ b/src/app/Observers/TransactionObserver.php @@ -0,0 +1,26 @@ +{$transaction->getKeyName()} = $allegedly_unique; + break; + } + } + } +} diff --git a/src/app/Observers/UserObserver.php b/src/app/Observers/UserObserver.php --- a/src/app/Observers/UserObserver.php +++ b/src/app/Observers/UserObserver.php @@ -111,9 +111,12 @@ // Entitlements do not have referential integrity on the entitled object, so this is our // way of doing an onDelete('cascade') without the foreign key. - Entitlement::where('entitleable_id', $user->id) - ->where('entitleable_type', User::class) - ->delete(); + $entitlements = Entitlement::where('entitleable_id', $user->id) + ->where('entitleable_type', User::class)->get(); + + foreach ($entitlements as $entitlement) { + $entitlement->delete(); + } // Remove owned users/domains $wallets = $user->wallets()->pluck('id')->all(); diff --git a/src/app/Providers/AppServiceProvider.php b/src/app/Providers/AppServiceProvider.php --- a/src/app/Providers/AppServiceProvider.php +++ b/src/app/Providers/AppServiceProvider.php @@ -32,6 +32,7 @@ \App\Plan::observe(\App\Observers\PlanObserver::class); \App\SignupCode::observe(\App\Observers\SignupCodeObserver::class); \App\Sku::observe(\App\Observers\SkuObserver::class); + \App\Transaction::observe(\App\Observers\TransactionObserver::class); \App\User::observe(\App\Observers\UserObserver::class); \App\UserAlias::observe(\App\Observers\UserAliasObserver::class); \App\UserSetting::observe(\App\Observers\UserSettingObserver::class); diff --git a/src/app/Providers/Payment/Stripe.php b/src/app/Providers/Payment/Stripe.php --- a/src/app/Providers/Payment/Stripe.php +++ b/src/app/Providers/Payment/Stripe.php @@ -328,6 +328,10 @@ } break; + + default: + \Log::debug("Unhandled Stripe event: " . var_export($payload, true)); + break; } return 200; diff --git a/src/app/Transaction.php b/src/app/Transaction.php new file mode 100644 --- /dev/null +++ b/src/app/Transaction.php @@ -0,0 +1,207 @@ + 'integer', + ]; + + /** @var boolean This model uses an automatically incrementing integer primary key? */ + public $incrementing = false; + + /** @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() + { + if ($this->object_type !== \App\Entitlement::class) { + return null; + } + + return \App\Entitlement::withTrashed()->where('id', $this->object_id)->first(); + } + + public function setTypeAttribute($value) + { + switch ($value) { + case self::ENTITLEMENT_BILLED: + case self::ENTITLEMENT_CREATED: + case self::ENTITLEMENT_DELETED: + // TODO: Must be an entitlement. + $this->attributes['type'] = $value; + break; + + case self::WALLET_AWARD: + case self::WALLET_CREDIT: + case self::WALLET_DEBIT: + case self::WALLET_PENALTY: + // TODO: This must be a wallet. + $this->attributes['type'] = $value; + break; + + default: + throw new \Exception("Invalid type value"); + } + } + + 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 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. + * + * @return int|null + */ + private function getEntitlementCost(): ?int + { + 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(); + + return $cost * $discount; + } + + /** + * Return the object email if any. This is the email for the target user entitlement. + * + * @return string|null + */ + private function getEntitlementObjectEmail(): ?string + { + $entitlement = $this->entitlement(); + + if (!$entitlement) { + return null; + } + + $entitleable = $entitlement->entitleable; + + if (!$entitleable) { + \Log::debug("No entitleable for {$entitlement->id} ?"); + return null; + } + + return $entitleable->email; + } + + /** + * Return the title for the SKU this entitlement is for. + * + * @return string|null + */ + private function getEntitlementSkuTitle(): ?string + { + if (!$this->entitlement()) { + return null; + } + + return $this->entitlement()->sku->{'title'}; + } + + /** + * Return the description for the wallet, if any, or 'default wallet'. + * + * @return string + */ + public function getWalletDescription() + { + $description = null; + + if ($entitlement = $this->entitlement()) { + $description = $entitlement->wallet->{'description'}; + } + + if ($wallet = $this->wallet()) { + $description = $wallet->{'description'}; + } + + return $description ?: 'Default wallet'; + } + + /** + * Get a string for use in translation tables derived from the object type. + * + * @return string|null + */ + private function objectTypeToLabelString(): ?string + { + if ($this->object_type == \App\Entitlement::class) { + return 'entitlement'; + } + + if ($this->object_type == \App\Wallet::class) { + return 'wallet'; + } + + return null; + } +} diff --git a/src/app/Utils.php b/src/app/Utils.php --- a/src/app/Utils.php +++ b/src/app/Utils.php @@ -3,6 +3,8 @@ namespace App; use App\Rules\UserEmailLocal; +use Carbon\Carbon; +use Illuminate\Support\Facades\Auth; use Illuminate\Support\Facades\Validator; use Illuminate\Support\Str; use Ramsey\Uuid\Uuid; @@ -13,6 +15,19 @@ class Utils { /** + * Return the number of days in the month prior to this one. + * + * @return int + */ + public static function daysInLastMonth() + { + $start = new Carbon('first day of last month'); + $end = new Carbon('last day of last month'); + + return $start->diffInDays($end); + } + + /** * Provide all unique combinations of elements in $input, with order and duplicates irrelevant. * * @param array $input The input array of elements. @@ -31,6 +46,22 @@ } /** + * Returns the current user's email address or null. + * + * @return string + */ + public static function userEmailOrNull(): ?string + { + $user = Auth::user(); + + if (!$user) { + return null; + } + + return $user->email; + } + + /** * Returns a UUID in the form of an integer. * * @return integer @@ -80,6 +111,27 @@ } /** + * Create self URL + * + * @param string $route Route/Path + * + * @return string Full URL + */ + public static function serviceUrl(string $route): string + { + $url = \url($route); + + $app_url = trim(\config('app.url'), '/'); + $pub_url = trim(\config('app.public_url'), '/'); + + if ($pub_url != $app_url) { + $url = str_replace($app_url, $pub_url, $url); + } + + return $url; + } + + /** * Create a configuration/environment data to be passed to * the UI * @@ -105,27 +157,6 @@ } /** - * Create self URL - * - * @param string $route Route/Path - * - * @return string Full URL - */ - public static function serviceUrl(string $route): string - { - $url = \url($route); - - $app_url = trim(\config('app.url'), '/'); - $pub_url = trim(\config('app.public_url'), '/'); - - if ($pub_url != $app_url) { - $url = str_replace($app_url, $pub_url, $url); - } - - return $url; - } - - /** * Email address (login or alias) validation * * @param string $email Email address diff --git a/src/app/Wallet.php b/src/app/Wallet.php --- a/src/app/Wallet.php +++ b/src/app/Wallet.php @@ -7,6 +7,7 @@ use Carbon\Carbon; use Iatstuti\Database\Support\NullableFields; use Illuminate\Database\Eloquent\Model; +use Illuminate\Support\Facades\DB; /** * The eloquent definition of a wallet -- a container with a chunk of change. @@ -60,8 +61,12 @@ public function chargeEntitlements($apply = true) { $charges = 0; - $discount = $this->discount ? $this->discount->discount : 0; - $discount = (100 - $discount) / 100; + $discount = $this->getDiscountRate(); + + DB::beginTransaction(); + + // used to parent individual entitlement billings to the wallet debit. + $entitlementTransactions = []; foreach ($this->entitlements()->get()->fresh() as $entitlement) { // This entitlement has been created less than or equal to 14 days ago (this is at @@ -91,47 +96,39 @@ $entitlement->updated_at = $entitlement->updated_at->copy()->addMonthsWithoutOverflow($diff); $entitlement->save(); - // TODO: This would be better done out of the loop (debit() will call save()), - // but then, maybe we should use a db transaction - $this->debit($cost); + if ($cost == 0) { + continue; + } + + $entitlementTransactions[] = $entitlement->createTransaction( + \App\Transaction::ENTITLEMENT_BILLED, + $cost + ); } } - return $charges; - } + if ($apply) { + $this->debit($charges, $entitlementTransactions); + } - /** - * The discount assigned to the wallet. - * - * @return \Illuminate\Database\Eloquent\Relations\BelongsTo - */ - public function discount() - { - return $this->belongsTo('App\Discount', 'discount_id', 'id'); - } + DB::commit(); - /** - * Calculate the expected charges to this wallet. - * - * @return int - */ - public function expectedCharges() - { - return $this->chargeEntitlements(false); + return $charges; } /** - * Remove a controller from this wallet. - * - * @param \App\User $user The user to remove as a controller from this wallet. + * Controllers of this wallet. * - * @return void + * @return \Illuminate\Database\Eloquent\Relations\BelongsToMany */ - public function removeController(User $user) + public function controllers() { - if ($this->controllers->contains($user)) { - $this->controllers()->detach($user); - } + return $this->belongsToMany( + 'App\User', // The foreign object definition + 'user_accounts', // The table name + 'wallet_id', // The local foreign key + 'user_id' // The remote foreign key + ); } /** @@ -147,38 +144,60 @@ $this->save(); + \App\Transaction::create( + [ + 'user_email' => \App\Utils::userEmailOrNull(), + 'object_id' => $this->id, + 'object_type' => \App\Wallet::class, + 'type' => \App\Transaction::WALLET_CREDIT, + 'amount' => $amount + ] + ); + return $this; } /** * Deduct an amount of pecunia from this wallet's balance. * - * @param int $amount The amount of pecunia to deduct (in cents). - * + * @param int $amount The amount of pecunia to deduct (in cents). + * @param array $eTIDs List of transaction IDs for the individual entitlements that make up + * this debit record, if any. * @return Wallet Self */ - public function debit(int $amount): Wallet + public function debit(int $amount, array $eTIDs = []): Wallet { + if ($amount == 0) { + return $this; + } + $this->balance -= $amount; $this->save(); + $transaction = \App\Transaction::create( + [ + 'user_email' => \App\Utils::userEmailOrNull(), + 'object_id' => $this->id, + 'object_type' => \App\Wallet::class, + 'type' => \App\Transaction::WALLET_DEBIT, + 'amount' => $amount + ] + ); + + \App\Transaction::whereIn('id', $eTIDs)->update(['transaction_id' => $transaction->id]); + return $this; } /** - * Controllers of this wallet. + * The discount assigned to the wallet. * - * @return \Illuminate\Database\Eloquent\Relations\BelongsToMany + * @return \Illuminate\Database\Eloquent\Relations\BelongsTo */ - public function controllers() + public function discount() { - return $this->belongsToMany( - 'App\User', // The foreign object definition - 'user_accounts', // The table name - 'wallet_id', // The local foreign key - 'user_id' // The remote foreign key - ); + return $this->belongsTo('App\Discount', 'discount_id', 'id'); } /** @@ -192,6 +211,38 @@ } /** + * Calculate the expected charges to this wallet. + * + * @return int + */ + public function expectedCharges() + { + return $this->chargeEntitlements(false); + } + + /** + * Return the exact, numeric version of the discount to be applied. + * + * Ranges from 0 - 100. + * + * @return int + */ + public function getDiscount() + { + return $this->discount ? $this->discount->discount : 0; + } + + /** + * The actual discount rate for use in multiplication + * + * Ranges from 0.00 to 1.00. + */ + public function getDiscountRate() + { + return (100 - $this->getDiscount()) / 100; + } + + /** * The owner of the wallet -- the wallet is in his/her back pocket. * * @return \Illuminate\Database\Eloquent\Relations\BelongsTo @@ -212,6 +263,20 @@ } /** + * Remove a controller from this wallet. + * + * @param \App\User $user The user to remove as a controller from this wallet. + * + * @return void + */ + public function removeController(User $user) + { + if ($this->controllers->contains($user)) { + $this->controllers()->detach($user); + } + } + + /** * Any (additional) properties of this wallet. * * @return \Illuminate\Database\Eloquent\Relations\HasMany @@ -220,4 +285,19 @@ { return $this->hasMany('App\WalletSetting'); } + + /** + * Retrieve the transactions against this wallet. + * + * @return iterable \App\Transaction + */ + public function transactions() + { + return \App\Transaction::where( + [ + 'object_id' => $this->id, + 'object_type' => \App\Wallet::class + ] + )->orderBy('created_at')->get(); + } } diff --git a/src/database/migrations/2014_10_12_000000_create_users_table.php b/src/database/migrations/2014_10_12_000000_create_users_table.php --- a/src/database/migrations/2014_10_12_000000_create_users_table.php +++ b/src/database/migrations/2014_10_12_000000_create_users_table.php @@ -4,6 +4,7 @@ use Illuminate\Database\Schema\Blueprint; use Illuminate\Database\Migrations\Migration; +// phpcs:ignore class CreateUsersTable extends Migration { /** @@ -21,7 +22,6 @@ $table->string('password')->nullable(); $table->string('password_ldap')->nullable(); $table->smallinteger('status'); -// $table->rememberToken(); $table->timestamps(); $table->primary('id'); diff --git a/src/database/migrations/2020_03_16_100000_create_payments.php b/src/database/migrations/2020_03_16_100000_create_payments.php --- a/src/database/migrations/2020_03_16_100000_create_payments.php +++ b/src/database/migrations/2020_03_16_100000_create_payments.php @@ -4,6 +4,7 @@ use Illuminate\Database\Schema\Blueprint; use Illuminate\Database\Migrations\Migration; +// phpcs:ignore class CreatePayments extends Migration { /** diff --git a/src/database/migrations/2020_05_21_080131_create_transactions_table.php b/src/database/migrations/2020_05_21_080131_create_transactions_table.php new file mode 100644 --- /dev/null +++ b/src/database/migrations/2020_05_21_080131_create_transactions_table.php @@ -0,0 +1,44 @@ +string('id', 36)->primary(); + $table->string('user_email')->nullable(); + $table->string('object_id', 36)->index(); + $table->string('object_type', 36)->index(); + $table->string('type', 8); + $table->integer('amount')->nullable(); + $table->string('description')->nullable(); + $table->string('transaction_id', 36)->nullable()->index(); + $table->timestamps(); + + $table->index(['object_id', 'object_type']); + } + ); + } + + /** + * Reverse the migrations. + * + * @return void + */ + public function down() + { + Schema::dropIfExists('transactions'); + } +} diff --git a/src/package-lock.json b/src/package-lock.json --- a/src/package-lock.json +++ b/src/package-lock.json @@ -833,12 +833,6 @@ "to-fast-properties": "^2.0.0" } }, - "@deveodk/vue-toastr": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@deveodk/vue-toastr/-/vue-toastr-1.1.0.tgz", - "integrity": "sha512-G+VontfAl088m/9MDfJ/+BxDIYmXAHZglMz+VOMTFgp9aPMt88P1b3xlJxA43xsQBot2tmcwAjNVphYIibtFpQ==", - "dev": true - }, "@fortawesome/fontawesome-common-types": { "version": "0.2.27", "resolved": "https://registry.npmjs.org/@fortawesome/fontawesome-common-types/-/fontawesome-common-types-0.2.27.tgz", @@ -1774,9 +1768,9 @@ "dev": true }, "bootstrap": { - "version": "4.4.1", - "resolved": "https://registry.npmjs.org/bootstrap/-/bootstrap-4.4.1.tgz", - "integrity": "sha512-tbx5cHubwE6e2ZG7nqM3g/FZ5PQEDMWmMGNrCUBVRPHXTJaH7CBDdsLeu3eCh3B1tzAxTnAbtmrzvWEvT2NNEA==", + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/bootstrap/-/bootstrap-4.5.0.tgz", + "integrity": "sha512-Z93QoXvodoVslA+PWNdk23Hze4RBYIkpb5h8I2HY2Tu2h7A0LpAgLcyrhrSUyo2/Oxm2l1fRZPs1e5hnxnliXA==", "dev": true }, "brace-expansion": { @@ -6076,9 +6070,9 @@ "dev": true }, "jquery": { - "version": "3.4.1", - "resolved": "https://registry.npmjs.org/jquery/-/jquery-3.4.1.tgz", - "integrity": "sha512-36+AdBzCL+y6qjw5Tx7HgzeGCzC81MDDgaUP8ld2zhx58HdqXGoBd+tHdrBMiyjGQs0Hxs/MLZTu/eHNJJuWPw==", + "version": "3.5.1", + "resolved": "https://registry.npmjs.org/jquery/-/jquery-3.5.1.tgz", + "integrity": "sha512-XwIBPqcMn57FxfT+Go5pzySnm4KWkT1Tv7gjrpT1srtf8Weynl6R273VJ5GjkRb51IzMp5nbaPjJXMWeju2MKg==", "dev": true }, "js-levenshtein": { @@ -7585,9 +7579,9 @@ } }, "popper.js": { - "version": "1.15.0", - "resolved": "https://registry.npmjs.org/popper.js/-/popper.js-1.15.0.tgz", - "integrity": "sha512-w010cY1oCUmI+9KwwlWki+r5jxKfTFDVoadl7MSrIujHU5MJ5OR6HTDj6Xo8aoR/QsA56x8jKjA59qGH4ELtrA==", + "version": "1.16.1", + "resolved": "https://registry.npmjs.org/popper.js/-/popper.js-1.16.1.tgz", + "integrity": "sha512-Wb4p1J4zyFTbM+u6WuO4XstYx4Ky9Cewe4DWrel7B0w6VVICvPwdOpotjzcf6eD8TsckVnIMNONQyPIUFOUbCQ==", "dev": true }, "portfinder": { diff --git a/src/resources/lang/en/transactions.php b/src/resources/lang/en/transactions.php new file mode 100644 --- /dev/null +++ b/src/resources/lang/en/transactions.php @@ -0,0 +1,12 @@ + ':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', + + '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' +]; diff --git a/src/tests/Feature/Controller/PaymentsMollieTest.php b/src/tests/Feature/Controller/PaymentsMollieTest.php --- a/src/tests/Feature/Controller/PaymentsMollieTest.php +++ b/src/tests/Feature/Controller/PaymentsMollieTest.php @@ -9,10 +9,11 @@ use App\WalletSetting; use GuzzleHttp\Psr7\Response; use Tests\TestCase; +use Tests\MollieMocksTrait; class PaymentsMollieTest extends TestCase { - use \Tests\MollieMocksTrait; + use MollieMocksTrait; /** * {@inheritDoc} diff --git a/src/tests/Feature/Controller/PaymentsStripeTest.php b/src/tests/Feature/Controller/PaymentsStripeTest.php --- a/src/tests/Feature/Controller/PaymentsStripeTest.php +++ b/src/tests/Feature/Controller/PaymentsStripeTest.php @@ -9,10 +9,11 @@ use App\WalletSetting; use GuzzleHttp\Psr7\Response; use Tests\TestCase; +use Tests\StripeMocksTrait; class PaymentsStripeTest extends TestCase { - use \Tests\StripeMocksTrait; + use StripeMocksTrait; /** * {@inheritDoc} diff --git a/src/tests/Unit/TransactionTest.php b/src/tests/Unit/TransactionTest.php new file mode 100644 --- /dev/null +++ b/src/tests/Unit/TransactionTest.php @@ -0,0 +1,61 @@ +assertNotNull($transaction->toString()); + } + } + + public function testWalletPenalty() + { + $user = $this->getTestUser('jane@kolabnow.com'); + $wallet = $user->wallets()->first(); + + $transaction = \App\Transaction::create( + [ + 'object_id' => $wallet->id, + 'object_type' => \App\Wallet::class, + 'type' => \App\Transaction::WALLET_PENALTY, + 'amount' => 9 + ] + ); + + $this->assertEquals($transaction->{'type'}, 'penalty'); + } + + public function testInvalidType() + { + $user = $this->getTestUser('jane@kolabnow.com'); + $wallet = $user->wallets()->first(); + + $this->expectException(\Exception::class); + + $transaction = \App\Transaction::create( + [ + 'object_id' => $wallet->id, + 'object_type' => \App\Wallet::class, + 'type' => 'invalid', + 'amount' => 9 + ] + ); + } +}