diff --git a/src/app/Http/Controllers/API/V4/Admin/DiscountsController.php b/src/app/Http/Controllers/API/V4/Admin/DiscountsController.php new file mode 100644 --- /dev/null +++ b/src/app/Http/Controllers/API/V4/Admin/DiscountsController.php @@ -0,0 +1,42 @@ +orderBy('discount')->get() + ->map(function ($discount) use (&$discounts) { + $label = $discount->discount . '% - ' . $discount->description; + + if ($discount->code) { + $label .= " [{$discount->code}]"; + } + + $discounts[] = [ + 'id' => $discount->id, + 'discount' => $discount->discount, + 'code' => $discount->code, + 'description' => $discount->description, + 'label' => $label, + ]; + }); + + return response()->json([ + 'status' => 'success', + 'list' => $discounts, + 'count' => count($discounts), + ]); + } +} 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 @@ -2,6 +2,48 @@ namespace App\Http\Controllers\API\V4\Admin; +use App\Discount; +use App\Wallet; +use Illuminate\Http\Request; + class WalletsController extends \App\Http\Controllers\API\V4\WalletsController { + /** + * Update wallet data. + * + * @param \Illuminate\Http\Request $request The API request. + * @params string $id Wallet identifier + * + * @return \Illuminate\Http\JsonResponse The response + */ + public function update(Request $request, $id) + { + $wallet = Wallet::find($id); + + if (empty($wallet)) { + return $this->errorResponse(404); + } + + if (array_key_exists('discount', $request->input())) { + if (empty($request->discount)) { + $wallet->discount()->dissociate(); + $wallet->save(); + } elseif ($discount = Discount::find($request->discount)) { + $wallet->discount()->associate($discount); + $wallet->save(); + } + } + + $response = $wallet->toArray(); + + if ($wallet->discount) { + $response['discount'] = $wallet->discount->discount; + $response['discount_description'] = $wallet->discount->description; + } + + $response['status'] = 'success'; + $response['message'] = \trans('app.wallet-update-success'); + + return response()->json($response); + } } 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 @@ -13,7 +13,7 @@ /** * Display a listing of the resource. * - * @return \Illuminate\Http\Response + * @return \Illuminate\Http\JsonResponse */ public function index() { @@ -23,7 +23,7 @@ /** * Show the form for creating a new resource. * - * @return \Illuminate\Http\Response + * @return \Illuminate\Http\JsonResponse */ public function create() { @@ -35,7 +35,7 @@ * * @param \Illuminate\Http\Request $request * - * @return \Illuminate\Http\Response + * @return \Illuminate\Http\JsonResponse */ public function store(Request $request) { @@ -47,7 +47,7 @@ * * @param int $id * - * @return \Illuminate\Http\Response + * @return \Illuminate\Http\JsonResponse */ public function show($id) { @@ -59,7 +59,7 @@ * * @param int $id * - * @return \Illuminate\Http\Response + * @return \Illuminate\Http\JsonResponse */ public function edit($id) { @@ -72,7 +72,7 @@ * @param \Illuminate\Http\Request $request * @param int $id * - * @return \Illuminate\Http\Response + * @return \Illuminate\Http\JsonResponse */ public function update(Request $request, $id) { @@ -84,7 +84,7 @@ * * @param int $id * - * @return \Illuminate\Http\Response + * @return \Illuminate\Http\JsonResponse */ public function destroy($id) { diff --git a/src/resources/lang/en/app.php b/src/resources/lang/en/app.php --- a/src/resources/lang/en/app.php +++ b/src/resources/lang/en/app.php @@ -27,4 +27,6 @@ 'search-foundxdomains' => ':x domains have been found.', 'search-foundxusers' => ':x user accounts have been found.', + + 'wallet-update-success' => 'User wallet updated successfully.', ]; diff --git a/src/resources/vue/Admin/User.vue b/src/resources/vue/Admin/User.vue --- a/src/resources/vue/Admin/User.vue +++ b/src/resources/vue/Admin/User.vue @@ -110,7 +110,7 @@
{{ wallet_discount ? (wallet_discount + '% - ' + wallet_discount_description) : 'none' }} - +
@@ -223,6 +223,33 @@ + + @@ -240,8 +267,10 @@ balance: 0, discount: 0, discount_description: '', + discounts: [], wallet_discount: 0, wallet_discount_description: '', + wallet_discount_id: '', domains: [], skus: [], users: [], @@ -281,6 +310,7 @@ }) this.wallet_discount = this.user.wallets[0].discount + this.wallet_discount_id = this.user.wallets[0].discount_id || '' this.wallet_discount_description = this.user.wallets[0].discount_description // Create subscriptions list @@ -293,6 +323,8 @@ let item = { id: sku.id, name: sku.name, + cost: sku.cost, + units: count - sku.units_free, price: this.price(sku.cost, count - sku.units_free) } @@ -329,6 +361,21 @@ }) }, methods: { + discountEdit() { + $('#discount-dialog') + .on('shown.bs.modal', (e, a) => { + $(e.target).find('select').focus() + }) + .modal() + + if (!this.discounts.length) { + // Fetch discounts + axios.get('/api/v4/discounts') + .then(response => { + this.discounts = response.data.list + }) + } + }, price(cost, units = 1) { let index = '' @@ -342,7 +389,30 @@ } return this.$root.price(cost * units) + '/month' + index - } + }, + submitDiscount() { + let dialog = $('#discount-dialog').modal('hide') + + axios.put('/api/v4/wallets/' + this.user.wallets[0].id, { discount: this.wallet_discount_id }) + .then(response => { + if (response.data.status == 'success') { + this.$toastr('success', response.data.message) + this.wallet_discount = response.data.discount + this.wallet_discount_id = response.data.discount_id || '' + this.wallet_discount_description = response.data.discount_description + + // Update prices in Subscriptions tab + if (this.user.wallet.id == response.data.id) { + this.discount = this.wallet_discount + this.discount_description = this.wallet_discount_description + + this.skus.forEach(sku => { + sku.price = this.price(sku.cost, sku.units) + }) + } + } + }) + }, } } diff --git a/src/routes/api.php b/src/routes/api.php --- a/src/routes/api.php +++ b/src/routes/api.php @@ -94,5 +94,6 @@ Route::apiResource('skus', API\V4\Admin\SkusController::class); Route::apiResource('users', API\V4\Admin\UsersController::class); Route::apiResource('wallets', API\V4\Admin\WalletsController::class); + Route::apiResource('discounts', API\V4\Admin\DiscountsController::class); } ); diff --git a/src/tests/Browser/Admin/UserTest.php b/src/tests/Browser/Admin/UserTest.php --- a/src/tests/Browser/Admin/UserTest.php +++ b/src/tests/Browser/Admin/UserTest.php @@ -4,6 +4,7 @@ use App\Discount; use Tests\Browser; +use Tests\Browser\Components\Dialog; use Tests\Browser\Components\Toast; use Tests\Browser\Pages\Admin\User as UserPage; use Tests\Browser\Pages\Dashboard; @@ -339,4 +340,63 @@ }); }); } + + /** + * Test editing wallet discount + * + * @depends testUserInfo2 + */ + public function testWalletDiscount(): void + { + $this->browse(function (Browser $browser) { + $john = $this->getTestUser('john@kolab.org'); + + $browser->visit(new UserPage($john->id)) + ->pause(100) + ->click('@user-finances #discount button') + // Test dialog content, and closing it with Cancel button + ->with(new Dialog('#discount-dialog'), function (Browser $browser) { + $browser->assertSeeIn('@title', 'Account discount') + ->assertFocused('@body select') + ->assertSelected('@body select', '') + ->assertSeeIn('@button-cancel', 'Cancel') + ->assertSeeIn('@button-action', 'Submit') + ->click('@button-cancel'); + }) + ->assertMissing('#discount-dialog') + ->click('@user-finances #discount button') + // Change the discount + ->with(new Dialog('#discount-dialog'), function (Browser $browser) { + $browser->click('@body select') + ->click('@body select option:nth-child(2)') + ->click('@button-action'); + }) + ->assertToast(Toast::TYPE_SUCCESS, '', 'User wallet updated successfully.') + ->assertSeeIn('#discount span', '10% - Test voucher') + ->click('@nav #tab-subscriptions') + ->with('@user-subscriptions', function (Browser $browser) { + $browser->assertSeeIn('table tbody tr:nth-child(1) td:last-child', '3,99 CHF/month¹') + ->assertSeeIn('table tbody tr:nth-child(2) td:last-child', '0,00 CHF/month¹') + ->assertSeeIn('table tbody tr:nth-child(3) td:last-child', '4,99 CHF/month¹') + ->assertSeeIn('table + .hint', '¹ applied discount: 10% - Test voucher'); + }) + // Change back to 'none' + ->click('@nav #tab-finances') + ->click('@user-finances #discount button') + ->with(new Dialog('#discount-dialog'), function (Browser $browser) { + $browser->click('@body select') + ->click('@body select option:nth-child(1)') + ->click('@button-action'); + }) + ->assertToast(Toast::TYPE_SUCCESS, '', 'User wallet updated successfully.') + ->assertSeeIn('#discount span', 'none') + ->click('@nav #tab-subscriptions') + ->with('@user-subscriptions', function (Browser $browser) { + $browser->assertSeeIn('table tbody tr:nth-child(1) td:last-child', '4,44 CHF/month') + ->assertSeeIn('table tbody tr:nth-child(2) td:last-child', '0,00 CHF/month') + ->assertSeeIn('table tbody tr:nth-child(3) td:last-child', '5,55 CHF/month') + ->assertMissing('table + .hint'); + }); + }); + } } diff --git a/src/tests/Feature/Controller/Admin/DiscountsTest.php b/src/tests/Feature/Controller/Admin/DiscountsTest.php new file mode 100644 --- /dev/null +++ b/src/tests/Feature/Controller/Admin/DiscountsTest.php @@ -0,0 +1,61 @@ +getTestUser('john@kolab.org'); + $admin = $this->getTestUser('jeroen@jeroen.jeroen'); + + // Non-admin user + $response = $this->actingAs($user)->get("api/v4/discounts"); + $response->assertStatus(403); + + // Admin user + $response = $this->actingAs($admin)->get("api/v4/discounts"); + $response->assertStatus(200); + + $json = $response->json(); + + $discount_test = Discount::where('code', 'TEST')->first(); + $discount_free = Discount::where('discount', 100)->first(); + + $this->assertSame(3, $json['count']); + $this->assertSame($discount_test->id, $json['list'][0]['id']); + $this->assertSame($discount_test->discount, $json['list'][0]['discount']); + $this->assertSame($discount_test->code, $json['list'][0]['code']); + $this->assertSame($discount_test->description, $json['list'][0]['description']); + $this->assertSame('10% - Test voucher [TEST]', $json['list'][0]['label']); + + $this->assertSame($discount_free->id, $json['list'][2]['id']); + $this->assertSame($discount_free->discount, $json['list'][2]['discount']); + $this->assertSame($discount_free->code, $json['list'][2]['code']); + $this->assertSame($discount_free->description, $json['list'][2]['description']); + $this->assertSame('100% - Free Account', $json['list'][2]['label']); + } +} diff --git a/src/tests/Feature/Controller/Admin/WalletsTest.php b/src/tests/Feature/Controller/Admin/WalletsTest.php new file mode 100644 --- /dev/null +++ b/src/tests/Feature/Controller/Admin/WalletsTest.php @@ -0,0 +1,70 @@ +getTestUser('john@kolab.org'); + $admin = $this->getTestUser('jeroen@jeroen.jeroen'); + $wallet = $user->wallets()->first(); + $discount = Discount::where('code', 'TEST')->first(); + + // Non-admin user + $response = $this->actingAs($user)->put("api/v4/wallets/{$wallet->id}", []); + $response->assertStatus(403); + + // Admin user - setting a discount + $post = ['discount' => $discount->id]; + $response = $this->actingAs($admin)->put("api/v4/wallets/{$wallet->id}", $post); + $response->assertStatus(200); + + $json = $response->json(); + + $this->assertSame('success', $json['status']); + $this->assertSame('User wallet updated successfully.', $json['message']); + $this->assertSame($wallet->id, $json['id']); + $this->assertSame($discount->discount, $json['discount']); + $this->assertSame($discount->id, $json['discount_id']); + $this->assertSame($discount->description, $json['discount_description']); + $this->assertSame($discount->id, $wallet->fresh()->discount->id); + + // Admin user - removing a discount + $post = ['discount' => null]; + $response = $this->actingAs($admin)->put("api/v4/wallets/{$wallet->id}", $post); + $response->assertStatus(200); + + $json = $response->json(); + + $this->assertSame('success', $json['status']); + $this->assertSame('User wallet updated successfully.', $json['message']); + $this->assertSame($wallet->id, $json['id']); + $this->assertSame(null, $json['discount_id']); + $this->assertTrue(empty($json['discount_description'])); + $this->assertSame(null, $wallet->fresh()->discount); + } +}