Merge branch 'fraud-prevention-mechanism'

# Conflicts:
#	public/chunks/admin.js
#	public/chunks/payments/settings.js
#	public/chunks/platform.js
#	public/chunks/settings.js
#	public/chunks/status-check.js
#	public/css/tailwind.css
#	public/js/main.js
#	public/mix-manifest.json
#	src/App/Providers/AppServiceProvider.php
#	tests/Domain/Admin/AdminTest.php
This commit is contained in:
Čarodej
2022-06-30 10:54:13 +02:00
25 changed files with 536 additions and 105 deletions

View File

@@ -108,7 +108,7 @@ return [
'want_to_delete_card_description' => 'We will no longer settle your payments automatically and you will have to fund your account for the next payments.',
'credit_card_deleted' => 'Your credit card was deleted.',
'billed_annually' => 'Billed Annually',
'restricted_account_warning' => 'Your functionality is restricted. Please review your billing settings.',
'restricted_account_warning' => 'Your functionality is restricted.',
'subscription_type' => 'Subscription Type',
'subscription_type_note' => 'Please do not change in production environment.',
'select_subscription_type' => 'Select your subscription type',
@@ -267,6 +267,22 @@ return [
'synchronizing_plans' => 'Synchronizing Plans...',
'plans_are_synchronizing' => 'Your plans are synchronizing with the payment gateways',
'plans_was_synchronized' => 'Plans was successfully synchronized',
'limit_usage_in_new_accounts_1_subject' => 'Please make first payment for your account to fund your usage',
'limit_usage_in_new_accounts_1_line' => 'We are happy you are using our service. To continue to using our service, please make first payment for your account balance to fund your usage.',
'limit_usage_in_new_accounts_2_subject' => '📆 Reminder: Please make first payment for your account to fund your usage',
'limit_usage_in_new_accounts_2_line' => 'We are happy you are using our service. To continue to using our service, please make first payment for your account balance to fund your usage.',
'limit_usage_in_new_accounts_3_subject' => '‼️ Uh-oh! Your functionality was restricted. Please make payment to continue using your account',
'limit_usage_in_new_accounts_3_line' => 'We are sorry for the inconvenience with using our service. To continue to using our service, please make first payment for your account balance to fund your usage and your functionality will be allowed as soon as possible.',
'usage_bigger_than_balance_1_subject' => "⚠️ You don't have sufficient funds in your account, please increase your account balance",
'usage_bigger_than_balance_1_line' => 'We are happy you are using our service. To continue to using our service, please increase your funds for your account balance to cover your usage.',
'usage_bigger_than_balance_2_subject' => "📆 Reminder: You don't have sufficient funds in your account, please increase your account balance",
'usage_bigger_than_balance_2_line' => 'We are happy you are using our service. To continue to using our service, please increase your funds for your account balance to cover your usage.',
'usage_bigger_than_balance_3_subject' => '‼️ Uh-oh! Your functionality was restricted. Please increase your funds for your account balance to cover your usage',
'usage_bigger_than_balance_3_line' => 'We are sorry for the inconvenience with using our service. To continue to using our service, please increase your funds for your account balance to cover your usage and your functionality will be allowed as soon as possible.',
'dunning_notification_description' => 'Please resolve your billing as soon as possible. Your functions can be restricted.',
'allow_limit_usage_in_new_accounts' => 'Allow limiting max usage before users will be forced to increase balance in first month of account existence',
'allow_limit_usage_bigger_than_balance' => 'Force users to increase balance when usage is bigger than their current balance',
'limit_usage_description_for_restrictions' => 'If user does not increase his balance or store his credit card, after third notification user account functionality will be restricted.',
],
'regular' => [
'type' => 'Type',

View File

@@ -23,6 +23,7 @@ return [
],
'notifications' => [
'DunningEmailToCoverAccountUsageNotification' => \Domain\Subscriptions\Notifications\DunningEmailToCoverAccountUsageNotification::class,
'ChargeFromCreditCardFailedAgainNotification' => \Domain\Subscriptions\Notifications\ChargeFromCreditCardFailedAgainNotification::class,
'ChargeFromCreditCardFailedNotification' => \Domain\Subscriptions\Notifications\ChargeFromCreditCardFailedNotification::class,
'SubscriptionWasCreatedNotification' => \Domain\Subscriptions\Notifications\SubscriptionWasCreatedNotification::class,
@@ -32,8 +33,18 @@ return [
'BonusCreditAddedNotification' => \Domain\Subscriptions\Notifications\BonusCreditAddedNotification::class,
],
'metered_billing' => [
'metered_billing' => [
'settlement_period' => 30,
'fraud_prevention_mechanism' => [
'usage_bigger_than_balance' => [
'active' => true,
],
'limit_usage_in_new_accounts' => [
'active' => true,
'amount' => 5,
],
],
],
'paystack' => [
@@ -48,4 +59,5 @@ return [
],
'is_demo' => env('APP_DEMO', false),
'is_local' => env('APP_ENV', 'production') === 'local',
];

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

2
public/js/main.js vendored

File diff suppressed because one or more lines are too long

View File

@@ -1,75 +1,75 @@
{
"/js/main.js": "/js/main.js",
"/chunks/request.js": "/chunks/request.js?id=20ebb21e134700e4",
"/chunks/request-upload.js": "/chunks/request-upload.js?id=f76b221beaf6440b",
"/chunks/request.js": "/chunks/request.js?id=386fae37c11630a4",
"/chunks/request-upload.js": "/chunks/request-upload.js?id=b631a474ab4b733b",
"/chunks/setup-wizard.js": "/chunks/setup-wizard.js?id=6122cca2fca4f02b",
"/chunks/status-check.js": "/chunks/status-check.js?id=1f8c33af0e532582",
"/chunks/purchase-code.js": "/chunks/purchase-code.js?id=ff951e86f15f71ae",
"/chunks/database.js": "/chunks/database.js?id=15105175c99fcb46",
"/chunks/environment.js": "/chunks/environment.js?id=12e7ceac54c77dd5",
"/chunks/app-setup.js": "/chunks/app-setup.js?id=3b1c0f1bdedcc613",
"/chunks/admin-account.js": "/chunks/admin-account.js?id=049ae73dfc365a44",
"/chunks/shared.js": "/chunks/shared.js?id=6a301afc0e280d72",
"/chunks/shared/browser.js": "/chunks/shared/browser.js?id=2bacd8aa31df621f",
"/chunks/shared/single-file.js": "/chunks/shared/single-file.js?id=e8d41e256380519c",
"/chunks/status-check.js": "/chunks/status-check.js?id=8ce1ff7720194e7e",
"/chunks/purchase-code.js": "/chunks/purchase-code.js?id=ce78be58a4b9683b",
"/chunks/database.js": "/chunks/database.js?id=0fb6901a348563e4",
"/chunks/environment.js": "/chunks/environment.js?id=99910849f57466b0",
"/chunks/app-setup.js": "/chunks/app-setup.js?id=298c9b93ac1fbef5",
"/chunks/admin-account.js": "/chunks/admin-account.js?id=aea96c6182067abf",
"/chunks/shared.js": "/chunks/shared.js?id=d0a7ce2d419f0ad3",
"/chunks/shared/browser.js": "/chunks/shared/browser.js?id=4810c8d00a1ae4c8",
"/chunks/shared/single-file.js": "/chunks/shared/single-file.js?id=f48b3ae6660450cc",
"/chunks/shared/authenticate.js": "/chunks/shared/authenticate.js?id=4d66b8a277a638a7",
"/chunks/not-found.js": "/chunks/not-found.js?id=901e26fdc77de6e0",
"/chunks/temporary-unavailable.js": "/chunks/temporary-unavailable.js?id=c3a4f158b971145f",
"/chunks/admin.js": "/chunks/admin.js?id=5ffb3d4e7de956d9",
"/chunks/dashboard.js": "/chunks/dashboard.js?id=12aa238219ec44e2",
"/chunks/admin.js": "/chunks/admin.js?id=5d611417aa87f58f",
"/chunks/dashboard.js": "/chunks/dashboard.js?id=849bbd9454296cc0",
"/chunks/invoices.js": "/chunks/invoices.js?id=c40b8b7925003659",
"/chunks/subscriptions.js": "/chunks/subscriptions.js?id=1d72d33f4d3e9355",
"/chunks/subscriptions.js": "/chunks/subscriptions.js?id=cdca1b82ffe52ff5",
"/chunks/pages.js": "/chunks/pages.js?id=15ab720d05126115",
"/chunks/page-edit.js": "/chunks/page-edit.js?id=651cd395d5a24dbc",
"/chunks/plans.js": "/chunks/plans.js?id=89b35f8720666147",
"/chunks/users.js": "/chunks/users.js?id=a340a505a2329b33",
"/chunks/user-create.js": "/chunks/user-create.js?id=3c6fdff76fe051ab",
"/chunks/plan-create/fixed.js": "/chunks/plan-create/fixed.js?id=73888464bb69d554",
"/chunks/plan-create/metered.js": "/chunks/plan-create/metered.js?id=01ea01e6b79e1127",
"/chunks/plan-create/fixed.js": "/chunks/plan-create/fixed.js?id=edab12aa27d01011",
"/chunks/plan-create/metered.js": "/chunks/plan-create/metered.js?id=d51d6ac54137e506",
"/chunks/user.js": "/chunks/user.js?id=85aee36020612960",
"/chunks/user-detail.js": "/chunks/user-detail.js?id=454ea88c61de9866",
"/chunks/user-storage.js": "/chunks/user-storage.js?id=5bf9038a2328dc1d",
"/chunks/user-subscription.js": "/chunks/user-subscription.js?id=d793e5bf87fe65a1",
"/chunks/user-password.js": "/chunks/user-password.js?id=984b7a3e1c377303",
"/chunks/user-delete.js": "/chunks/user-delete.js?id=50314acbbfccabd1",
"/chunks/user-detail.js": "/chunks/user-detail.js?id=7db7b89f55411710",
"/chunks/user-storage.js": "/chunks/user-storage.js?id=fc2f3e4a3848093d",
"/chunks/user-subscription.js": "/chunks/user-subscription.js?id=ece5195df551c9a5",
"/chunks/user-password.js": "/chunks/user-password.js?id=a3d07775fab6ba38",
"/chunks/user-delete.js": "/chunks/user-delete.js?id=e7db32c7ae79379b",
"/chunks/plan.js": "/chunks/plan.js?id=4a082719fc5ce20a",
"/chunks/plan-subscribers.js": "/chunks/plan-subscribers.js?id=3f87f55441ea6acb",
"/chunks/plan-settings.js": "/chunks/plan-settings.js?id=59aa29c4d81f665e",
"/chunks/plan-delete.js": "/chunks/plan-delete.js?id=627e6562f74ae332",
"/chunks/plan-subscribers.js": "/chunks/plan-subscribers.js?id=6c62bd162c9194b5",
"/chunks/plan-settings.js": "/chunks/plan-settings.js?id=aa5152c40d77c398",
"/chunks/plan-delete.js": "/chunks/plan-delete.js?id=b552da027e7ccdc8",
"/chunks/payments.js": "/chunks/payments.js?id=22e84a36acc89129",
"/chunks/payments/billings.js": "/chunks/payments/billings.js?id=ced5adcb7cf4c6c9",
"/chunks/payments/settings.js": "/chunks/payments/settings.js?id=9f8a84a4cae9baa7",
"/chunks/payments/billings.js": "/chunks/payments/billings.js?id=8cf2287d221825c1",
"/chunks/payments/settings.js": "/chunks/payments/settings.js?id=cebdae0e4d02d3d1",
"/chunks/app-settings.js": "/chunks/app-settings.js?id=5de958be12ca920f",
"/chunks/app-appearance.js": "/chunks/app-appearance.js?id=6851c76bf345605f",
"/chunks/app-index.js": "/chunks/app-index.js?id=f914fa6136ff18e1",
"/chunks/app-appearance.js": "/chunks/app-appearance.js?id=bc104e3407af9abb",
"/chunks/app-index.js": "/chunks/app-index.js?id=96b505173e1be922",
"/chunks/app-environment.js": "/chunks/app-environment.js?id=037f1adeccd04620",
"/chunks/app-others.js": "/chunks/app-others.js?id=c0ccc32c1eee6162",
"/chunks/app-sign-in-out.js": "/chunks/app-sign-in-out.js?id=580f3b376cfc93e4",
"/chunks/app-adsense.js": "/chunks/app-adsense.js?id=304eefb5acbe5d93",
"/chunks/app-server.js": "/chunks/app-server.js?id=02bf261940010e9f",
"/chunks/app-language.js": "/chunks/app-language.js?id=33e69eb36f8578be",
"/chunks/app-server.js": "/chunks/app-server.js?id=6571d4b614fdd0bd",
"/chunks/app-language.js": "/chunks/app-language.js?id=b701f4a95999f1c1",
"/chunks/homepage.js": "/chunks/homepage.js?id=612d0b10b26b580c",
"/chunks/dynamic-page.js": "/chunks/dynamic-page.js?id=13d00de1153769c5",
"/chunks/contact-us.js": "/chunks/contact-us.js?id=5573693525bfcb27",
"/chunks/contact-us.js": "/chunks/contact-us.js?id=08cc61f27823a5d1",
"/chunks/demo.js": "/chunks/demo.js?id=aebe316fa638bbb5",
"/chunks/successfully-email-verified.js": "/chunks/successfully-email-verified.js?id=62ed454569571e25",
"/chunks/successfully-email-send.js": "/chunks/successfully-email-send.js?id=d99d1e9adb974ae7",
"/chunks/sign-in.js": "/chunks/sign-in.js?id=6280ef0a858522c8",
"/chunks/sign-up.js": "/chunks/sign-up.js?id=8ca080b244f1647f",
"/chunks/sign-up.js": "/chunks/sign-up.js?id=caeb50ac27194d33",
"/chunks/forgotten-password.js": "/chunks/forgotten-password.js?id=b06174390d32669c",
"/chunks/create-new-password.js": "/chunks/create-new-password.js?id=3fe56e872c74d485",
"/chunks/settings.js": "/chunks/settings.js?id=47159da321e4ba8d",
"/chunks/settings.js": "/chunks/settings.js?id=cb4fc111071ec323",
"/chunks/profile.js": "/chunks/profile.js?id=87ac69edc17d9245",
"/chunks/settings-password.js": "/chunks/settings-password.js?id=6ee89249d080df79",
"/chunks/settings-password.js": "/chunks/settings-password.js?id=99e9984bfcd5289b",
"/chunks/settings-storage.js": "/chunks/settings-storage.js?id=a9e2543c5362e459",
"/chunks/billing.js": "/chunks/billing.js?id=073f5b46e2ef61bf",
"/chunks/platform.js": "/chunks/platform.js?id=f13965206e1e2fad",
"/chunks/files.js": "/chunks/files.js?id=f55e9f88919e0925",
"/chunks/recent-uploads.js": "/chunks/recent-uploads.js?id=847e1649fa88b1fc",
"/chunks/my-shared-items.js": "/chunks/my-shared-items.js?id=f36265974d8237cd",
"/chunks/trash.js": "/chunks/trash.js?id=07d9deb9d1ff75b5",
"/chunks/team-folders.js": "/chunks/team-folders.js?id=10215d85d3d37932",
"/chunks/shared-with-me.js": "/chunks/shared-with-me.js?id=cfe08cfe150cd917",
"/chunks/billing.js": "/chunks/billing.js?id=c4160a2491437905",
"/chunks/platform.js": "/chunks/platform.js?id=62d94e384c593749",
"/chunks/files.js": "/chunks/files.js?id=5251b1cdad4ad5ad",
"/chunks/recent-uploads.js": "/chunks/recent-uploads.js?id=b1f5ab7af810aa91",
"/chunks/my-shared-items.js": "/chunks/my-shared-items.js?id=44dd934482c86e2e",
"/chunks/trash.js": "/chunks/trash.js?id=ee453a73e86e2501",
"/chunks/team-folders.js": "/chunks/team-folders.js?id=d2951d6319e4ded3",
"/chunks/shared-with-me.js": "/chunks/shared-with-me.js?id=f1cbe65db0e882de",
"/chunks/invitation.js": "/chunks/invitation.js?id=dd7d9c7bb0f39eff",
"/css/tailwind.css": "/css/tailwind.css",
"/css/app.css": "/css/app.css"

View File

@@ -19,7 +19,7 @@
class="vue-feather text-theme shrink-0"
/>
<alert-triangle-icon
v-if="['billing-alert', 'insufficient-balance'].includes(notification.data.attributes.category)"
v-if="['billing-alert', 'insufficient-balance', 'payment-alert'].includes(notification.data.attributes.category)"
size="22"
class="vue-feather text-theme shrink-0"
/>

View File

@@ -1,7 +1,7 @@
<template>
<div v-if="$store.getters.isLimitedUser" class="bg-red-500 py-1 text-center">
<div v-if="$store.getters.userLimitationReason" class="bg-gradient-to-r from-red-600 to-red-500 py-2.5 px-1 text-center leading-none">
<router-link :to="{ name: 'Billing' }" class="text-xs font-bold text-white">
{{ $t('restricted_account_warning') }}
{{ $t('restricted_account_warning') + ' ' + $store.getters.userLimitationReason }}
</router-link>
</div>
</template>

View File

@@ -201,10 +201,7 @@ const mutations = {
}
const getters = {
isLimitedUser: (state) =>
state.user &&
state.user.data.relationships.failedPayments &&
state.user.data.relationships.failedPayments.data.length === 3,
userLimitationReason: (state) => state.user && state.user.data.meta.restrictions.reason,
permission: (state) => state.permission,
user: (state) => state.user,
}

View File

@@ -75,6 +75,49 @@
</AppInputButton>
</div>
<!--Fraud Prevention Mechanism Rules-->
<div v-if="config.subscriptionType === 'metered' && allowedPayments" class="card shadow-card">
<FormLabel icon="shield">
{{ $t('Fraud Prevention Mechanism Rules') }}
</FormLabel>
<AppInputSwitch
:title="$t('allow_limit_usage_in_new_accounts')"
:description="$t('limit_usage_description_for_restrictions')"
>
<SwitchInput
@input="$updateText('/admin/settings', 'limit_usage_in_new_accounts', settings.limitUsageInNewAccounts)"
v-model="settings.limitUsageInNewAccounts"
:state="settings.limitUsageInNewAccounts"
/>
</AppInputSwitch>
<AppInputText
v-if="settings.limitUsageInNewAccounts"
class="-mt-3"
>
<input
@input="$updateText('/admin/settings', 'limit_usage_in_new_accounts_amount', settings.limitUsageInNewAccountsAmount)"
v-model="settings.limitUsageInNewAccountsAmount"
:placeholder="$t('Max Usage Amount...')"
type="number"
class="focus-border-theme input-dark"
/>
</AppInputText>
<AppInputSwitch
:title="$t('allow_limit_usage_bigger_than_balance')"
:description="$t('limit_usage_description_for_restrictions')"
:is-last="true"
>
<SwitchInput
@input="$updateText('/admin/settings', 'usage_bigger_than_balance', settings.usageBiggerThanBalance)"
v-model="settings.usageBiggerThanBalance"
:state="settings.usageBiggerThanBalance"
/>
</AppInputSwitch>
</div>
<!--Stripe method configuration-->
<div v-if="allowedPayments" class="card shadow-card">
<img :src="$getPaymentLogo('stripe')" alt="Stripe" class="mb-8 h-8" />
@@ -518,10 +561,13 @@ export default {
}
},
computed: {
...mapGetters(['config']),
...mapGetters([
'config'
]),
},
data() {
return {
settings: undefined,
allowedRegistrationBonus: true,
registrationBonusAmount: undefined,
@@ -661,6 +707,23 @@ export default {
})
}
},
mounted() {
axios
.get('/api/admin/settings', {
params: {
column: 'limit_usage_in_new_accounts|limit_usage_in_new_accounts_amount|usage_bigger_than_balance',
},
})
.then((response) => {
this.isLoading = false
this.settings = {
limitUsageInNewAccounts: parseInt(response.data.limit_usage_in_new_accounts),
limitUsageInNewAccountsAmount: parseInt(response.data.limit_usage_in_new_accounts_amount),
usageBiggerThanBalance: parseInt(response.data.usage_bigger_than_balance),
}
})
},
created() {
// Set payment description
this.stripe.paymentDescription = this.config.stripe_payment_description

View File

@@ -111,7 +111,7 @@ export default {
DragUI,
},
computed: {
...mapGetters(['isVisibleSidebar', 'isLimitedUser', 'config', 'currentFolder']),
...mapGetters(['isVisibleSidebar', 'config', 'currentFolder']),
},
data() {
return {

View File

@@ -27,6 +27,49 @@ class AppServiceProvider extends ServiceProvider
// TODO: temporary
config()->set('session.lifetime', 15120);
// Set subscription config
$this->setSubscriptionConfig();
// Set app locale
$this->setLocale();
// Get all migrations with all directories
$this->setMigrations();
}
private function setMigrations(): void
{
$mainPath = database_path('migrations');
$directories = glob($mainPath . '/*', GLOB_ONLYDIR);
$this->loadMigrationsFrom(
array_merge([$mainPath], $directories)
);
}
private function setSubscriptionConfig(): void
{
if (app()->runningUnitTests()) {
return;
}
$settings = getAllSettings();
config([
'subscription.metered_billing.fraud_prevention_mechanism' => [
'usage_bigger_than_balance' => [
'active' => isset($settings->usage_bigger_than_balance) ? intval($settings->usage_bigger_than_balance) : true,
],
'limit_usage_in_new_accounts' => [
'active' => isset($settings->limit_usage_in_new_accounts) ? intval($settings->limit_usage_in_new_accounts) : true,
'amount' => isset($settings->limit_usage_in_new_accounts_amount) ? intval($settings->limit_usage_in_new_accounts_amount) : 20,
],
]
]);
}
private function setLocale(): void
{
try {
$app_locale = get_settings('language') ?? 'en';
} catch (\PDOException $e) {
@@ -38,21 +81,5 @@ class AppServiceProvider extends ServiceProvider
// Set locale for carbon dates
setlocale(LC_TIME, $app_locale . '_' . mb_strtoupper($app_locale));
// Get all migrations with all directories
$this->loadMigrationsFrom(
$this->get_migration_paths()
);
}
/**
* @return array
*/
private function get_migration_paths(): array
{
$mainPath = database_path('migrations');
$directories = glob($mainPath . '/*', GLOB_ONLYDIR);
return array_merge([$mainPath], $directories);
}
}

View File

@@ -3,6 +3,7 @@ namespace App\Users\Models;
use ByteUnits\Metric;
use Illuminate\Support\Str;
use BadMethodCallException;
use Domain\Files\Models\File;
use Domain\Folders\Models\Folder;
use Laravel\Sanctum\HasApiTokens;
@@ -196,10 +197,14 @@ class User extends Authenticatable implements MustVerifyEmail
public function __call($method, $parameters)
{
if (str_starts_with($method, 'can')) {
return resolve(RestrictionsManager::class)
->driver()
->$method($this, ...$parameters);
try {
if (str_starts_with($method, 'can') || str_starts_with($method, 'get')) {
return resolve(RestrictionsManager::class)
->driver()
->$method($this, ...$parameters);
}
} catch (BadMethodCallException $e) {
return parent::__call($method, $parameters);
}
return parent::__call($method, $parameters);

View File

@@ -69,6 +69,7 @@ class UserResource extends JsonResource
'canCreateFolder' => $this->canCreateFolder(),
'canCreateTeamFolder' => $this->canCreateTeamFolder(),
'canInviteTeamMembers' => $this->canInviteTeamMembers(),
'reason' => $this->getRestrictionReason(),
],
$this->mergeWhen($isFixedSubscription, fn () => [
'limitations' => $this->limitations->summary(),

View File

@@ -47,4 +47,9 @@ class DefaultRestrictionsEngine implements RestrictionsEngine
{
return true;
}
public function getRestrictionReason(User $user): string|null
{
return null;
}
}

View File

@@ -43,4 +43,9 @@ class FixedBillingRestrictionsEngine implements RestrictionsEngine
{
return true;
}
public function getRestrictionReason(User $user): string|null
{
return null;
}
}

View File

@@ -8,26 +8,46 @@ class MeteredBillingRestrictionsEngine implements RestrictionsEngine
{
public function canUpload(User $user, int $fileSize = 0): bool
{
// Check the count of the dunning emails
if ($this->getDunningSequenceCount($user) === 3) {
return false;
}
// Disable upload when user has more than 3 failed payments
return ! ($user->failedPayments()->count() >= 3);
return $this->checkFailedPayments($user);
}
public function canDownload(User $user): bool
{
// Check the count of the dunning emails
if ($this->getDunningSequenceCount($user) === 3) {
return false;
}
// Disable download when user has more than 3 failed payments
return ! ($user->failedPayments()->count() >= 3);
return $this->checkFailedPayments($user);
}
public function canCreateFolder(User $user): bool
{
// Check the count of the dunning emails
if ($this->getDunningSequenceCount($user) === 3) {
return false;
}
// Disable create folder when user has more than 3 failed payments
return ! ($user->failedPayments()->count() >= 3);
return $this->checkFailedPayments($user);
}
public function canCreateTeamFolder(User $user): bool
{
// Check the count of the dunning emails
if ($this->getDunningSequenceCount($user) === 3) {
return false;
}
// Disable create folder when user has more than 3 failed payments
return ! ($user->failedPayments()->count() >= 3);
return $this->checkFailedPayments($user);
}
public function canInviteTeamMembers(User $user, array $newInvites = []): bool
@@ -37,7 +57,38 @@ class MeteredBillingRestrictionsEngine implements RestrictionsEngine
public function canVisitShared(User $user): bool
{
// Check the count of the dunning emails
if ($this->getDunningSequenceCount($user) === 3) {
return false;
}
// Disable share visit when user has more than 3 failed payments
return ! ($user->failedPayments()->count() >= 3);
return $this->checkFailedPayments($user);
}
public function getRestrictionReason(User $user): string|null
{
if ($this->getDunningSequenceCount($user) === 3) {
return match ($user->dunning->type) {
'limit_usage_in_new_accounts' => 'Please make your first payment to cover your usage.',
'usage_bigger_than_balance' => 'Please increase your account balance higher than your monthly usage.',
};
}
if (! $this->checkFailedPayments($user)) {
return 'Please update your credit card to pay your usage.';
}
return null;
}
private function getDunningSequenceCount(User $user): int
{
return cache()->remember("dunning-count.$user->id", 3600, fn () => $user?->dunning->sequence ?? 0);
}
private function checkFailedPayments(User $user): bool
{
return cache()->remember("failed-payments-count.$user->id", 3600, fn () => !($user->failedPayments()->count() >= 3));
}
}

View File

@@ -16,4 +16,6 @@ interface RestrictionsEngine
public function canInviteTeamMembers(User $user, array $newInvites = []): bool;
public function canVisitShared(User $user): bool;
public function getRestrictionReason(User $user): string|null;
}

View File

@@ -0,0 +1,89 @@
<?php
namespace Domain\Subscriptions\Notifications;
use Illuminate\Bus\Queueable;
use Illuminate\Notifications\Notification;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Notifications\Messages\MailMessage;
use VueFileManager\Subscription\Domain\DunningEmails\Models\Dunning;
class DunningEmailToCoverAccountUsageNotification extends Notification implements ShouldQueue
{
use Queueable;
public function __construct(
private Dunning $dunning
) {
}
public function via(): array
{
return ['mail', 'database', 'broadcast'];
}
public function toMail(): MailMessage
{
$message = $this->dunningMessages();
$index = $this->dunning->sequence - 1;
return (new MailMessage)
->subject($message[$this->dunning->type][$index]['subject'])
->greeting(__('Hi there'))
->line($message[$this->dunning->type][$index]['line'])
->action(__t('show_billing'), url('/user/settings/billing'))
->salutation(__('Regards'));
}
public function toArray(): array
{
$message = $this->dunningMessages();
$index = $this->dunning->sequence - 1;
return [
'category' => 'payment-alert',
'title' => $message[$this->dunning->type][$index]['subject'],
'description' => __t('dunning_notification_description'),
'action' => [
'type' => 'route',
'params' => [
'route' => __t('billing'),
'button' => __t('show_billing'),
],
],
];
}
private function dunningMessages(): array
{
return [
'limit_usage_in_new_accounts' => [
[
'subject' => __t('limit_usage_in_new_accounts_1_subject'),
'line' => __t('limit_usage_in_new_accounts_1_line'),
],
[
'subject' => __t('limit_usage_in_new_accounts_2_subject'),
'line' => __t('limit_usage_in_new_accounts_2_line'),
],
[
'subject' => __t('limit_usage_in_new_accounts_3_subject'),
'line' => __t('limit_usage_in_new_accounts_3_line'),
],
],
'usage_bigger_than_balance' => [
[
'subject' => __t("usage_bigger_than_balance_1_subject"),
'line' => __t('usage_bigger_than_balance_1_line'),
],
[
'subject' => __t('usage_bigger_than_balance_2_subject'),
'line' => __t('usage_bigger_than_balance_2_line'),
],
[
'subject' => __t('usage_bigger_than_balance_3_subject'),
'line' => __t('usage_bigger_than_balance_3_line'),
],
],
];
}
}

View File

@@ -1,4 +1,5 @@
<?php
namespace Tests\App\Restrictions;
use Illuminate\Http\UploadedFile;
@@ -9,6 +10,7 @@ use App\Users\Models\User;
use Domain\Files\Models\File;
use Domain\Sharing\Models\Share;
use Domain\Settings\Models\Setting;
use VueFileManager\Subscription\Domain\DunningEmails\Models\Dunning;
class MeteredBillingRestrictionsTest extends TestCase
{
@@ -17,7 +19,7 @@ class MeteredBillingRestrictionsTest extends TestCase
parent::setUp();
Setting::updateOrCreate([
'name' => 'subscription_type',
'name' => 'subscription_type',
], [
'value' => 'metered',
]);
@@ -32,6 +34,13 @@ class MeteredBillingRestrictionsTest extends TestCase
->hasFailedpayments(2)
->create();
Dunning::factory()
->createOneQuietly([
'type' => 'limit_usage_in_new_accounts',
'user_id' => $user->id,
'sequence' => 2,
]);
$this->assertEquals(true, $user->canUpload());
}
@@ -47,6 +56,24 @@ class MeteredBillingRestrictionsTest extends TestCase
$this->assertEquals(false, $user->canUpload());
}
/**
* @test
*/
public function it_cant_upload_because_user_has_3_dunning_mails()
{
$user = User::factory()
->create();
Dunning::factory()
->createOneQuietly([
'type' => 'limit_usage_in_new_accounts',
'user_id' => $user->id,
'sequence' => 3,
]);
$this->assertEquals(false, $user->canUpload());
}
/**
* @test
*/
@@ -117,6 +144,47 @@ class MeteredBillingRestrictionsTest extends TestCase
$this->assertDatabaseCount('folders', 0);
}
/**
* @test
*/
public function it_cant_create_new_folder_because_user_has_3_dunning_mails()
{
$user = User::factory()
->create();
Dunning::factory()
->createOneQuietly([
'type' => 'limit_usage_in_new_accounts',
'user_id' => $user->id,
'sequence' => 3,
]);
// Create basic folder
$this
->actingAs($user)
->postJson('/api/create-folder', [
'name' => 'New Folder',
])
->assertStatus(401);
// Create team folder
$this
->actingAs($user)
->postJson('/api/teams/folders', [
'name' => 'New Folder',
'invitations' => [
[
'email' => 'john@doe.com',
'permission' => 'can-edit',
'type' => 'invitation',
],
],
])
->assertStatus(401);
$this->assertDatabaseCount('folders', 0);
}
/**
* @test
*/
@@ -139,6 +207,34 @@ class MeteredBillingRestrictionsTest extends TestCase
->assertStatus(401);
}
/**
* @test
*/
public function it_cant_get_private_file_because_user_has_3_dunning_mails()
{
$user = User::factory()
->create();
Dunning::factory()
->createOneQuietly([
'type' => 'limit_usage_in_new_accounts',
'user_id' => $user->id,
'sequence' => 3,
]);
$file = File::factory()
->create([
'user_id' => $user->id,
'basename' => 'fake-file.pdf',
'name' => 'fake-file.pdf',
]);
$this
->actingAs($user)
->get("file/$file->name")
->assertStatus(401);
}
/**
* @test
*/
@@ -160,11 +256,10 @@ class MeteredBillingRestrictionsTest extends TestCase
'name' => 'fake-file.pdf',
]);
// 404 but, ok, because there is not stored temporary file in test
$this
->actingAs($user)
->get("file/$file->name")
->assertStatus(404);
->get("file/$file->basename")
->assertStatus(200);
}
/**
@@ -196,6 +291,41 @@ class MeteredBillingRestrictionsTest extends TestCase
->assertStatus(401);
}
/**
* @test
*/
public function it_cant_get_shared_file_because_user_has_3_dunning_mails()
{
$user = User::factory()
->create();
Dunning::factory()
->createOneQuietly([
'type' => 'limit_usage_in_new_accounts',
'user_id' => $user->id,
'sequence' => 3,
]);
$file = File::factory()
->create([
'user_id' => $user->id,
'basename' => 'fake-file.pdf',
'name' => 'fake-file.pdf',
]);
$share = Share::factory()
->create([
'item_id' => $file->id,
'user_id' => $user->id,
'type' => 'file',
'is_protected' => false,
]);
$this
->get("file/$file->name/shared/$share->token")
->assertStatus(401);
}
/**
* @test
*/
@@ -234,7 +364,7 @@ class MeteredBillingRestrictionsTest extends TestCase
/**
* @test
*/
public function it_cant_get_share_page()
public function it_cant_get_share_page_because_user_has_3_failed_payments()
{
$user = User::factory()
->hasFailedpayments(3)
@@ -250,4 +380,30 @@ class MeteredBillingRestrictionsTest extends TestCase
$this->get("/share/$share->token")
->assertRedirect('/temporary-unavailable');
}
/**
* @test
*/
public function it_cant_get_share_page_because_user_has_3_dunning_mails()
{
$user = User::factory()
->create();
Dunning::factory()
->createOneQuietly([
'type' => 'limit_usage_in_new_accounts',
'user_id' => $user->id,
'sequence' => 3,
]);
$share = Share::factory()
->create([
'user_id' => $user->id,
'type' => 'folder',
'is_protected' => false,
]);
$this->get("/share/$share->token")
->assertRedirect('/temporary-unavailable');
}
}

View File

@@ -1,5 +1,4 @@
<?php
namespace Tests\Domain\Admin;
use Storage;
@@ -46,7 +45,8 @@ class AdminTest extends TestCase
->create(['role' => 'admin']);
$users->each(
fn($user) => $this
fn ($user) =>
$this
->actingAs($admin)
->getJson('/api/admin/users?page=1')
->assertStatus(200)
@@ -170,7 +170,7 @@ class AdminTest extends TestCase
])->assertStatus(200);
$this->assertDatabaseHas('user_settings', [
'user_id' => $user->id,
'user_id' => $user->id,
])->assertDatabaseHas('user_limitations', [
'max_storage_amount' => 10,
]);
@@ -215,13 +215,13 @@ class AdminTest extends TestCase
$this
->actingAs($admin)
->postJson('/api/admin/users', [
'name' => 'John Doe',
'role' => 'user',
'email' => 'john@doe.com',
'password' => 'VerySecretPassword',
'max_storage_amount' => 15,
'password_confirmation' => 'VerySecretPassword',
'avatar' => $avatar,
'name' => 'John Doe',
'role' => 'user',
'email' => 'john@doe.com',
'password' => 'VerySecretPassword',
'max_storage_amount' => 15,
'password_confirmation' => 'VerySecretPassword',
'avatar' => $avatar,
])->assertStatus(201);
$this->assertDatabaseHas('users', [
@@ -292,10 +292,10 @@ class AdminTest extends TestCase
->create("fake-file-$index.pdf", 1200, 'application/pdf');
$this->postJson('/api/upload/chunks', [
'name' => $file->name,
'extension' => 'pdf',
'chunk' => $file,
'is_last_chunk' => 1,
'name' => $file->name,
'extension' => 'pdf',
'chunk' => $file,
'is_last_chunk' => 1,
])->assertStatus(201);
});
@@ -317,6 +317,8 @@ class AdminTest extends TestCase
$admin = User::factory()
->create(['role' => 'admin']);
Sanctum::actingAs($admin);
// Delete user
$this
->actingAs($admin)
@@ -359,6 +361,6 @@ class AdminTest extends TestCase
});
Storage::disk('local')
->assertMissing($user->settings->avatar);
->assertMissing($user->settings->getRawOriginal('avatar'));
}
}