diff --git a/app/Console/Kernel.php b/app/Console/Kernel.php index 24a661eae68f..1d3be1016fce 100644 --- a/app/Console/Kernel.php +++ b/app/Console/Kernel.php @@ -11,6 +11,7 @@ namespace App\Console; +use App\Jobs\Cron\BillingSubscriptionCron; use App\Jobs\Cron\RecurringInvoicesCron; use App\Jobs\Ninja\AdjustEmailQuota; use App\Jobs\Ninja\CompanySizeCheck; @@ -53,6 +54,8 @@ class Kernel extends ConsoleKernel $schedule->job(new UpdateExchangeRates)->daily()->withoutOverlapping(); + $schedule->job(new BillingSubscriptionCron)->daily()->withoutOverlapping(); + $schedule->job(new RecurringInvoicesCron)->hourly()->withoutOverlapping(); /* Run hosted specific jobs */ diff --git a/app/Events/BillingSubscription/BillingSubscriptionWasCreated.php b/app/Events/BillingSubscription/BillingSubscriptionWasCreated.php new file mode 100644 index 000000000000..73fc2d1f2f87 --- /dev/null +++ b/app/Events/BillingSubscription/BillingSubscriptionWasCreated.php @@ -0,0 +1,55 @@ +billing_subscription = $billing_subscription; + $this->company = $company; + $this->event_vars = $event_vars; + } + + /** + * Get the channels the event should broadcast on. + * + * @return \Illuminate\Broadcasting\Channel|array + */ + public function broadcastOn() + { + return new PrivateChannel('channel-name'); + } +} diff --git a/app/Factory/BillingSubscriptionFactory.php b/app/Factory/BillingSubscriptionFactory.php new file mode 100644 index 000000000000..f9f051293e9c --- /dev/null +++ b/app/Factory/BillingSubscriptionFactory.php @@ -0,0 +1,27 @@ +company_id = $company_id; + $billing_subscription->user_id = $user_id; + + return $billing_subscription; + } +} diff --git a/app/Http/Controllers/BillingSubscriptionController.php b/app/Http/Controllers/BillingSubscriptionController.php new file mode 100644 index 000000000000..199d51005114 --- /dev/null +++ b/app/Http/Controllers/BillingSubscriptionController.php @@ -0,0 +1,410 @@ +billing_subscription_repo = $billing_subscription_repo; + } + + /** + * Show the list of BillingSubscriptions. + * + * @return Response + * + * @OA\Get( + * path="/api/v1/billing_subscriptions", + * operationId="getBillingSubscriptions", + * tags={"billing_subscriptions"}, + * summary="Gets a list of billing_subscriptions", + * description="Lists billing_subscriptions.", + * + * @OA\Parameter(ref="#/components/parameters/X-Api-Secret"), + * @OA\Parameter(ref="#/components/parameters/X-Api-Token"), + * @OA\Parameter(ref="#/components/parameters/X-Requested-With"), + * @OA\Parameter(ref="#/components/parameters/include"), + * @OA\Response( + * response=200, + * description="A list of billing_subscriptions", + * @OA\Header(header="X-MINIMUM-CLIENT-VERSION", ref="#/components/headers/X-MINIMUM-CLIENT-VERSION"), + * @OA\Header(header="X-RateLimit-Remaining", ref="#/components/headers/X-RateLimit-Remaining"), + * @OA\Header(header="X-RateLimit-Limit", ref="#/components/headers/X-RateLimit-Limit"), + * @OA\JsonContent(ref="#/components/schemas/BillingSubscription"), + * ), + * @OA\Response( + * response=422, + * description="Validation error", + * @OA\JsonContent(ref="#/components/schemas/ValidationError"), + * ), + * @OA\Response( + * response="default", + * description="Unexpected Error", + * @OA\JsonContent(ref="#/components/schemas/Error"), + * ), + * ) + */ + + public function index(): \Illuminate\Http\Response + { + $billing_subscriptions = BillingSubscription::query()->company(); + + return $this->listResponse($billing_subscriptions); + } + + /** + * Show the form for creating a new resource. + * + * @param CreateBillingSubscriptionRequest $request The request + * + * @return Response + * + * + * @OA\Get( + * path="/api/v1/billing_subscriptions/create", + * operationId="getBillingSubscriptionsCreate", + * tags={"billing_subscriptions"}, + * summary="Gets a new blank billing_subscriptions object", + * description="Returns a blank object with default values", + * @OA\Parameter(ref="#/components/parameters/X-Api-Secret"), + * @OA\Parameter(ref="#/components/parameters/X-Api-Token"), + * @OA\Parameter(ref="#/components/parameters/X-Requested-With"), + * @OA\Parameter(ref="#/components/parameters/include"), + * @OA\Response( + * response=200, + * description="A blank billing_subscriptions object", + * @OA\Header(header="X-MINIMUM-CLIENT-VERSION", ref="#/components/headers/X-MINIMUM-CLIENT-VERSION"), + * @OA\Header(header="X-RateLimit-Remaining", ref="#/components/headers/X-RateLimit-Remaining"), + * @OA\Header(header="X-RateLimit-Limit", ref="#/components/headers/X-RateLimit-Limit"), + * @OA\JsonContent(ref="#/components/schemas/BillingSubscription"), + * ), + * @OA\Response( + * response=422, + * description="Validation error", + * @OA\JsonContent(ref="#/components/schemas/ValidationError"), + * + * ), + * @OA\Response( + * response="default", + * description="Unexpected Error", + * @OA\JsonContent(ref="#/components/schemas/Error"), + * ), + * ) + */ + public function create(CreateBillingSubscriptionRequest $request): \Illuminate\Http\Response + { + $billing_subscription = BillingSubscriptionFactory::create(auth()->user()->company()->id, auth()->user()->id); + + return $this->itemResponse($billing_subscription); + } + + /** + * Store a newly created resource in storage. + * + * @param StoreBillingSubscriptionRequest $request The request + * + * @return Response + * + * + * @OA\Post( + * path="/api/v1/billing_subscriptions", + * operationId="storeBillingSubscription", + * tags={"billing_subscriptions"}, + * summary="Adds a billing_subscriptions", + * description="Adds an billing_subscriptions to the system", + * @OA\Parameter(ref="#/components/parameters/X-Api-Secret"), + * @OA\Parameter(ref="#/components/parameters/X-Api-Token"), + * @OA\Parameter(ref="#/components/parameters/X-Requested-With"), + * @OA\Parameter(ref="#/components/parameters/include"), + * @OA\Response( + * response=200, + * description="Returns the saved billing_subscriptions object", + * @OA\Header(header="X-MINIMUM-CLIENT-VERSION", ref="#/components/headers/X-MINIMUM-CLIENT-VERSION"), + * @OA\Header(header="X-RateLimit-Remaining", ref="#/components/headers/X-RateLimit-Remaining"), + * @OA\Header(header="X-RateLimit-Limit", ref="#/components/headers/X-RateLimit-Limit"), + * @OA\JsonContent(ref="#/components/schemas/BillingSubscription"), + * ), + * @OA\Response( + * response=422, + * description="Validation error", + * @OA\JsonContent(ref="#/components/schemas/ValidationError"), + * + * ), + * @OA\Response( + * response="default", + * description="Unexpected Error", + * @OA\JsonContent(ref="#/components/schemas/Error"), + * ), + * ) + */ + public function store(StoreBillingSubscriptionRequest $request): \Illuminate\Http\Response + { + $billing_subscription = $this->billing_subscription_repo->save($request->all(), BillingSubscriptionFactory::create(auth()->user()->company()->id, auth()->user()->id)); + + event(new BillingsubscriptionWasCreated($billing_subscription, $billing_subscription->company, Ninja::eventVars())); + + return $this->itemResponse($billing_subscription); + } + + /** + * Display the specified resource. + * + * @param ShowBillingSubscriptionRequest $request The request + * @param Invoice $billing_subscription The invoice + * + * @return Response + * + * + * @OA\Get( + * path="/api/v1/billing_subscriptions/{id}", + * operationId="showBillingSubscription", + * tags={"billing_subscriptions"}, + * summary="Shows an billing_subscriptions", + * description="Displays an billing_subscriptions by id", + * @OA\Parameter(ref="#/components/parameters/X-Api-Secret"), + * @OA\Parameter(ref="#/components/parameters/X-Api-Token"), + * @OA\Parameter(ref="#/components/parameters/X-Requested-With"), + * @OA\Parameter(ref="#/components/parameters/include"), + * @OA\Parameter( + * name="id", + * in="path", + * description="The BillingSubscription Hashed ID", + * example="D2J234DFA", + * required=true, + * @OA\Schema( + * type="string", + * format="string", + * ), + * ), + * @OA\Response( + * response=200, + * description="Returns the BillingSubscription object", + * @OA\Header(header="X-MINIMUM-CLIENT-VERSION", ref="#/components/headers/X-MINIMUM-CLIENT-VERSION"), + * @OA\Header(header="X-RateLimit-Remaining", ref="#/components/headers/X-RateLimit-Remaining"), + * @OA\Header(header="X-RateLimit-Limit", ref="#/components/headers/X-RateLimit-Limit"), + * @OA\JsonContent(ref="#/components/schemas/BillingSubscription"), + * ), + * @OA\Response( + * response=422, + * description="Validation error", + * @OA\JsonContent(ref="#/components/schemas/ValidationError"), + * + * ), + * @OA\Response( + * response="default", + * description="Unexpected Error", + * @OA\JsonContent(ref="#/components/schemas/Error"), + * ), + * ) + */ + public function show(ShowBillingSubscriptionRequest $request, BillingSubscription $billing_subscription): \Illuminate\Http\Response + { + return $this->itemResponse($billing_subscription); + } + + /** + * Show the form for editing the specified resource. + * + * @param EditBillingSubscriptionRequest $request The request + * @param Invoice $billing_subscription The invoice + * + * @return Response + * + * @OA\Get( + * path="/api/v1/billing_subscriptions/{id}/edit", + * operationId="editBillingSubscription", + * tags={"billing_subscriptions"}, + * summary="Shows an billing_subscriptions for editting", + * description="Displays an billing_subscriptions by id", + * @OA\Parameter(ref="#/components/parameters/X-Api-Secret"), + * @OA\Parameter(ref="#/components/parameters/X-Api-Token"), + * @OA\Parameter(ref="#/components/parameters/X-Requested-With"), + * @OA\Parameter(ref="#/components/parameters/include"), + * @OA\Parameter( + * name="id", + * in="path", + * description="The BillingSubscription Hashed ID", + * example="D2J234DFA", + * required=true, + * @OA\Schema( + * type="string", + * format="string", + * ), + * ), + * @OA\Response( + * response=200, + * description="Returns the invoice object", + * @OA\Header(header="X-MINIMUM-CLIENT-VERSION", ref="#/components/headers/X-MINIMUM-CLIENT-VERSION"), + * @OA\Header(header="X-RateLimit-Remaining", ref="#/components/headers/X-RateLimit-Remaining"), + * @OA\Header(header="X-RateLimit-Limit", ref="#/components/headers/X-RateLimit-Limit"), + * @OA\JsonContent(ref="#/components/schemas/BillingSubscription"), + * ), + * @OA\Response( + * response=422, + * description="Validation error", + * @OA\JsonContent(ref="#/components/schemas/ValidationError"), + * + * ), + * @OA\Response( + * response="default", + * description="Unexpected Error", + * @OA\JsonContent(ref="#/components/schemas/Error"), + * ), + * ) + */ + public function edit(EditBillingSubscriptionRequest $request, BillingSubscription $billing_subscription): \Illuminate\Http\Response + { + return $this->itemResponse($billing_subscription); + } + + /** + * Update the specified resource in storage. + * + * @param UpdateBillingSubscriptionRequest $request The request + * @param BillingSubscription $billing_subscription The invoice + * + * @return Response + * + * + * @OA\Put( + * path="/api/v1/billing_subscriptions/{id}", + * operationId="updateBillingSubscription", + * tags={"billing_subscriptions"}, + * summary="Updates an billing_subscriptions", + * description="Handles the updating of an billing_subscriptions by id", + * @OA\Parameter(ref="#/components/parameters/X-Api-Secret"), + * @OA\Parameter(ref="#/components/parameters/X-Api-Token"), + * @OA\Parameter(ref="#/components/parameters/X-Requested-With"), + * @OA\Parameter(ref="#/components/parameters/include"), + * @OA\Parameter( + * name="id", + * in="path", + * description="The BillingSubscription Hashed ID", + * example="D2J234DFA", + * required=true, + * @OA\Schema( + * type="string", + * format="string", + * ), + * ), + * @OA\Response( + * response=200, + * description="Returns the billing_subscriptions object", + * @OA\Header(header="X-MINIMUM-CLIENT-VERSION", ref="#/components/headers/X-MINIMUM-CLIENT-VERSION"), + * @OA\Header(header="X-RateLimit-Remaining", ref="#/components/headers/X-RateLimit-Remaining"), + * @OA\Header(header="X-RateLimit-Limit", ref="#/components/headers/X-RateLimit-Limit"), + * @OA\JsonContent(ref="#/components/schemas/BillingSubscription"), + * ), + * @OA\Response( + * response=422, + * description="Validation error", + * @OA\JsonContent(ref="#/components/schemas/ValidationError"), + * + * ), + * @OA\Response( + * response="default", + * description="Unexpected Error", + * @OA\JsonContent(ref="#/components/schemas/Error"), + * ), + * ) + */ + public function update(UpdateBillingSubscriptionRequest $request, BillingSubscription $billing_subscription) + { + if ($request->entityIsDeleted($billing_subscription)) { + return $request->disallowUpdate(); + } + + $billing_subscription = $this->billing_subscription_repo->save($request->all(), $billing_subscription); + + return $this->itemResponse($billing_subscription); + } + + /** + * Remove the specified resource from storage. + * + * @param DestroyBillingSubscriptionRequest $request + * @param BillingSubscription $invoice + * + * @return Response + * + * @throws \Exception + * @OA\Delete( + * path="/api/v1/billing_subscriptions/{id}", + * operationId="deleteBillingSubscription", + * tags={"billing_subscriptions"}, + * summary="Deletes a billing_subscriptions", + * description="Handles the deletion of an billing_subscriptions by id", + * @OA\Parameter(ref="#/components/parameters/X-Api-Secret"), + * @OA\Parameter(ref="#/components/parameters/X-Api-Token"), + * @OA\Parameter(ref="#/components/parameters/X-Requested-With"), + * @OA\Parameter(ref="#/components/parameters/include"), + * @OA\Parameter( + * name="id", + * in="path", + * description="The BillingSubscription Hashed ID", + * example="D2J234DFA", + * required=true, + * @OA\Schema( + * type="string", + * format="string", + * ), + * ), + * @OA\Response( + * response=200, + * description="Returns a HTTP status", + * @OA\Header(header="X-MINIMUM-CLIENT-VERSION", ref="#/components/headers/X-MINIMUM-CLIENT-VERSION"), + * @OA\Header(header="X-RateLimit-Remaining", ref="#/components/headers/X-RateLimit-Remaining"), + * @OA\Header(header="X-RateLimit-Limit", ref="#/components/headers/X-RateLimit-Limit"), + * ), + * @OA\Response( + * response=422, + * description="Validation error", + * @OA\JsonContent(ref="#/components/schemas/ValidationError"), + * + * ), + * @OA\Response( + * response="default", + * description="Unexpected Error", + * @OA\JsonContent(ref="#/components/schemas/Error"), + * ), + * ) + */ + public function destroy(DestroyBillingSubscriptionRequest $request, BillingSubscription $billing_subscription): \Illuminate\Http\Response + { + $this->billing_subscription_repo->delete($billing_subscription); + + return $this->itemResponse($billing_subscription->fresh()); + } +} diff --git a/app/Http/Controllers/OpenAPI/BillingSubscription.php b/app/Http/Controllers/OpenAPI/BillingSubscription.php new file mode 100644 index 000000000000..848a318542e6 --- /dev/null +++ b/app/Http/Controllers/OpenAPI/BillingSubscription.php @@ -0,0 +1,31 @@ +user()->can('create', BillingSubscription::class); + } + + /** + * Get the validation rules that apply to the request. + * + * @return array + */ + public function rules() + { + return [ + // + ]; + } +} diff --git a/app/Http/Requests/BillingSubscription/DestroyBillingSubscriptionRequest.php b/app/Http/Requests/BillingSubscription/DestroyBillingSubscriptionRequest.php new file mode 100644 index 000000000000..2abde3a9b43d --- /dev/null +++ b/app/Http/Requests/BillingSubscription/DestroyBillingSubscriptionRequest.php @@ -0,0 +1,31 @@ +user()->can('edit', $this->billing_subscription); + } + + /** + * Get the validation rules that apply to the request. + * + * @return array + */ + public function rules() + { + return [ + // + ]; + } +} diff --git a/app/Http/Requests/BillingSubscription/EditBillingSubscriptionRequest.php b/app/Http/Requests/BillingSubscription/EditBillingSubscriptionRequest.php new file mode 100644 index 000000000000..74538a8a79f9 --- /dev/null +++ b/app/Http/Requests/BillingSubscription/EditBillingSubscriptionRequest.php @@ -0,0 +1,31 @@ +user()->can('edit', $this->billing_subscription); + } + + /** + * Get the validation rules that apply to the request. + * + * @return array + */ + public function rules() + { + return [ + // + ]; + } +} diff --git a/app/Http/Requests/BillingSubscription/ShowBillingSubscriptionRequest.php b/app/Http/Requests/BillingSubscription/ShowBillingSubscriptionRequest.php new file mode 100644 index 000000000000..e5ee4828f3da --- /dev/null +++ b/app/Http/Requests/BillingSubscription/ShowBillingSubscriptionRequest.php @@ -0,0 +1,31 @@ +user()->can('view', $this->billing_subscription); + } + + /** + * Get the validation rules that apply to the request. + * + * @return array + */ + public function rules() + { + return [ + // + ]; + } +} diff --git a/app/Http/Requests/BillingSubscription/StoreBillingSubscriptionRequest.php b/app/Http/Requests/BillingSubscription/StoreBillingSubscriptionRequest.php new file mode 100644 index 000000000000..58d99d06d1f5 --- /dev/null +++ b/app/Http/Requests/BillingSubscription/StoreBillingSubscriptionRequest.php @@ -0,0 +1,51 @@ +user()->can('create', BillingSubscription::class); + } + + /** + * Get the validation rules that apply to the request. + * + * @return array + */ + public function rules() + { + return [ + 'user_id' => ['sometimes'], + 'product_id' => ['sometimes'], + 'assigned_user_id' => ['sometimes'], + 'company_id' => ['sometimes'], + 'is_recurring' => ['sometimes'], + 'frequency_id' => ['sometimes'], + 'auto_bill' => ['sometimes'], + 'promo_code' => ['sometimes'], + 'promo_discount' => ['sometimes'], + 'is_amount_discount' => ['sometimes'], + 'allow_cancellation' => ['sometimes'], + 'per_set_enabled' => ['sometimes'], + 'min_seats_limit' => ['sometimes'], + 'max_seats_limit' => ['sometimes'], + 'trial_enabled' => ['sometimes'], + 'trial_duration' => ['sometimes'], + 'allow_query_overrides' => ['sometimes'], + 'allow_plan_changes' => ['sometimes'], + 'plan_map' => ['sometimes'], + 'refund_period' => ['sometimes'], + 'webhook_configuration' => ['sometimes'], + ]; + } +} diff --git a/app/Http/Requests/BillingSubscription/UpdateBillingSubscriptionRequest.php b/app/Http/Requests/BillingSubscription/UpdateBillingSubscriptionRequest.php new file mode 100644 index 000000000000..2436def991ed --- /dev/null +++ b/app/Http/Requests/BillingSubscription/UpdateBillingSubscriptionRequest.php @@ -0,0 +1,33 @@ +user()->can('edit', $this->billing_subscription); + } + + /** + * Get the validation rules that apply to the request. + * + * @return array + */ + public function rules() + { + return [ + // + ]; + } +} diff --git a/app/Jobs/Cron/BillingSubscriptionCron.php b/app/Jobs/Cron/BillingSubscriptionCron.php new file mode 100644 index 000000000000..091cb10c5f83 --- /dev/null +++ b/app/Jobs/Cron/BillingSubscriptionCron.php @@ -0,0 +1,71 @@ +loopSubscriptions(); + } else { + //multiDB environment, need to + foreach (MultiDB::$dbs as $db) { + + MultiDB::setDB($db); + $this->loopSubscriptions(); + + } + } + } + + private function loopSubscriptions() + { + $client_subs = ClientSubscription::whereNull('deleted_at') + ->cursor() + ->each(function ($cs){ + $this->processSubscription($cs); + }); + } + + /* Our daily cron should check + + 1. Is the subscription still in trial phase? + 2. Check the recurring invoice and its remaining_cycles to see whether we need to cancel or perform any other function. + 3. Any notifications that need to fire? + */ + private function processSubscription($client_subscription) + { + + } +} diff --git a/app/Models/BillingSubscription.php b/app/Models/BillingSubscription.php new file mode 100644 index 000000000000..486e3134d607 --- /dev/null +++ b/app/Models/BillingSubscription.php @@ -0,0 +1,71 @@ + 'boolean', + 'plan_map' => 'object', + 'webhook_configuration' => 'object', + 'updated_at' => 'timestamp', + 'created_at' => 'timestamp', + 'deleted_at' => 'timestamp', + ]; + + public function company(): \Illuminate\Database\Eloquent\Relations\BelongsTo + { + return $this->belongsTo(Company::class); + } + + public function user(): \Illuminate\Database\Eloquent\Relations\BelongsTo + { + return $this->belongsTo(User::class); + } + + public function product(): \Illuminate\Database\Eloquent\Relations\BelongsTo + { + return $this->belongsTo(Product::class); + } + +} diff --git a/app/Models/ClientSubscription.php b/app/Models/ClientSubscription.php new file mode 100644 index 000000000000..ac7000cea2c0 --- /dev/null +++ b/app/Models/ClientSubscription.php @@ -0,0 +1,21 @@ +isAdmin() || $user->hasPermission('create_billing_subscription') || $user->hasPermission('create_all'); + } +} diff --git a/app/Providers/AppServiceProvider.php b/app/Providers/AppServiceProvider.php index 3d96377fa950..c017572692c3 100644 --- a/app/Providers/AppServiceProvider.php +++ b/app/Providers/AppServiceProvider.php @@ -12,7 +12,9 @@ namespace App\Providers; use App\Models\Account; +use App\Models\BillingSubscription; use App\Models\Client; +use App\Models\ClientSubscription; use App\Models\Company; use App\Models\CompanyGateway; use App\Models\CompanyToken; @@ -26,7 +28,9 @@ use App\Models\Quote; use App\Models\Task; use App\Models\User; use App\Observers\AccountObserver; +use App\Observers\BillingSubscriptionObserver; use App\Observers\ClientObserver; +use App\Observers\ClientSubscriptionObserver; use App\Observers\CompanyGatewayObserver; use App\Observers\CompanyObserver; use App\Observers\CompanyTokenObserver; @@ -75,9 +79,10 @@ class AppServiceProvider extends ServiceProvider Schema::defaultStringLength(191); - User::observe(UserObserver::class); Account::observe(AccountObserver::class); + BillingSubscription::observe(BillingSubscriptionObserver::class); Client::observe(ClientObserver::class); + ClientSubscription::observe(ClientSubscriptionObserver::class); Company::observe(CompanyObserver::class); CompanyGateway::observe(CompanyGatewayObserver::class); CompanyToken::observe(CompanyTokenObserver::class); @@ -89,6 +94,7 @@ class AppServiceProvider extends ServiceProvider Proposal::observe(ProposalObserver::class); Quote::observe(QuoteObserver::class); Task::observe(TaskObserver::class); + User::observe(UserObserver::class); // Queue::before(function (JobProcessing $event) { // // \Log::info('Event Job '.$event->connectionName); diff --git a/app/Providers/AuthServiceProvider.php b/app/Providers/AuthServiceProvider.php index 23d6f3a59220..60281db3b3fe 100644 --- a/app/Providers/AuthServiceProvider.php +++ b/app/Providers/AuthServiceProvider.php @@ -12,6 +12,7 @@ namespace App\Providers; use App\Models\Activity; +use App\Models\BillingSubscription; use App\Models\Client; use App\Models\Company; use App\Models\CompanyGateway; @@ -37,6 +38,7 @@ use App\Models\User; use App\Models\Vendor; use App\Models\Webhook; use App\Policies\ActivityPolicy; +use App\Policies\BillingSubscriptionPolicy; use App\Policies\ClientPolicy; use App\Policies\CompanyGatewayPolicy; use App\Policies\CompanyPolicy; @@ -73,6 +75,7 @@ class AuthServiceProvider extends ServiceProvider */ protected $policies = [ Activity::class => ActivityPolicy::class, + BillingSubscription::class => BillingSubscriptionPolicy::class, Client::class => ClientPolicy::class, Company::class => CompanyPolicy::class, CompanyToken::class => CompanyTokenPolicy::class, diff --git a/app/Repositories/BillingSubscriptionRepository.php b/app/Repositories/BillingSubscriptionRepository.php new file mode 100644 index 000000000000..68be2473468d --- /dev/null +++ b/app/Repositories/BillingSubscriptionRepository.php @@ -0,0 +1,28 @@ +fill($data) + ->save(); + + return $billing_subscription; + } +} diff --git a/app/Transformers/BillingSubscriptionTransformer.php b/app/Transformers/BillingSubscriptionTransformer.php new file mode 100644 index 000000000000..8adbd2568ac2 --- /dev/null +++ b/app/Transformers/BillingSubscriptionTransformer.php @@ -0,0 +1,73 @@ + $this->encodePrimaryKey($billing_subscription->id), + 'user_id' => $this->encodePrimaryKey($billing_subscription->user_id), + 'product_id' => $this->encodePrimaryKey($billing_subscription->product_id), + 'assigned_user_id' => $this->encodePrimaryKey($billing_subscription->assigned_user_id), + 'company_id' => $this->encodePrimaryKey($billing_subscription->company_id), + 'is_recurring' => (bool)$billing_subscription->is_recurring, + 'frequency_id' => (string)$billing_subscription->frequency_id, + 'auto_bill' => (string)$billing_subscription->auto_bill, + 'promo_code' => (string)$billing_subscription->promo_code, + 'promo_discount' => (float)$billing_subscription->promo_discount, + 'is_amount_discount' => (bool)$billing_subscription->is_amount_discount, + 'allow_cancellation' => (bool)$billing_subscription->allow_cancellation, + 'per_seat_enabled' => (bool)$billing_subscription->per_set_enabled, + 'min_seats_limit' => (int)$billing_subscription->min_seats_limit, + 'max_seats_limit' => (int)$billing_subscription->max_seats_limit, + 'trial_enabled' => (bool)$billing_subscription->trial_enabled, + 'trial_duration' => (int)$billing_subscription->trial_duration, + 'allow_query_overrides' => (bool)$billing_subscription->allow_query_overrides, + 'allow_plan_changes' => (bool)$billing_subscription->allow_plan_changes, + 'plan_map' => (string)$billing_subscription->plan_map, + 'refund_period' => (int)$billing_subscription->refund_period, + 'webhook_configuration' => (string)$billing_subscription->webhook_configuration, + 'is_deleted' => (bool)$billing_subscription->is_deleted, + 'created_at' => (int)$billing_subscription->created_at, + 'updated_at' => (int)$billing_subscription->updated_at, + 'archived_at' => (int)$billing_subscription->deleted_at, + ]; + } + + public function includeProduct(BillingSubscription $billing_subscription): \League\Fractal\Resource\Item + { + $transformer = new ProductTransformer($this->serializer); + + return $this->includeItem($billing_subscription->product, $transformer, Product::class); + } +} diff --git a/config/queue.php b/config/queue.php index 6fb44cc4f7a1..dc2d4665ea47 100644 --- a/config/queue.php +++ b/config/queue.php @@ -13,7 +13,7 @@ return [ | */ - 'default' => env('QUEUE_CONNECTION', 'database'), + 'default' => env('QUEUE_CONNECTION', 'sync'), /* |-------------------------------------------------------------------------- diff --git a/database/factories/BillingSubscriptionFactory.php b/database/factories/BillingSubscriptionFactory.php new file mode 100644 index 000000000000..2fdf320cd5f4 --- /dev/null +++ b/database/factories/BillingSubscriptionFactory.php @@ -0,0 +1,38 @@ +increments('id'); + $table->unsignedInteger('user_id'); + $table->unsignedInteger('assigned_user_id'); + $table->unsignedInteger('company_id'); + $table->unsignedInteger('product_id'); + $table->boolean('is_recurring')->default(false); + $table->unsignedInteger('frequency_id'); + $table->string('auto_bill')->default(''); + $table->string('promo_code')->default(''); + $table->float('promo_discount')->default(0); + $table->boolean('is_amount_discount')->default(false); + $table->boolean('allow_cancellation')->default(true); + $table->boolean('per_seat_enabled')->default(false); + $table->unsignedInteger('min_seats_limit'); + $table->unsignedInteger('max_seats_limit'); + $table->boolean('trial_enabled')->default(false); + $table->unsignedInteger('trial_duration'); + $table->boolean('allow_query_overrides')->default(false); + $table->boolean('allow_plan_changes')->default(false); + $table->mediumText('plan_map'); + $table->unsignedInteger('refund_period')->nullable(); + $table->mediumText('webhook_configuration'); + $table->softDeletes('deleted_at', 6); + $table->boolean('is_deleted')->default(false); + $table->timestamps(); + $table->foreign('product_id')->references('id')->on('products'); + $table->foreign('company_id')->references('id')->on('companies')->onDelete('cascade'); + $table->index(['company_id', 'deleted_at']); + }); + + Schema::create('client_subscriptions', function (Blueprint $table) { + $table->increments('id'); + $table->unsignedInteger('subscription_id'); + $table->unsignedInteger('recurring_invoice_id'); + $table->unsignedInteger('client_id'); + $table->unsignedInteger('trial_started')->nullable(); + $table->unsignedInteger('trial_ends')->nullable(); + $table->timestamps(); + $table->foreign('subscription_id')->references('id')->on('billing_subscriptions'); + $table->foreign('recurring_invoice_id')->references('id')->on('recurring_invoices'); + $table->foreign('client_id')->references('id')->on('clients'); + }); + } + + /** + * Reverse the migrations. + * + * @return void + */ + public function down() + { + Schema::dropIfExists('billing_subscriptions'); + Schema::dropIfExists('client_subscriptions'); + } +} diff --git a/database/migrations/2021_03_09_132242_add_currency_id_to_billing_subscriptions_table.php b/database/migrations/2021_03_09_132242_add_currency_id_to_billing_subscriptions_table.php new file mode 100644 index 000000000000..bc687317a449 --- /dev/null +++ b/database/migrations/2021_03_09_132242_add_currency_id_to_billing_subscriptions_table.php @@ -0,0 +1,29 @@ +unsignedInteger('currency_id')->nullable(); + }); + } + + /** + * Reverse the migrations. + * + * @return void + */ + public function down() + { + } +} diff --git a/routes/api.php b/routes/api.php index a503f03d9765..1af077d5fbeb 100644 --- a/routes/api.php +++ b/routes/api.php @@ -68,7 +68,7 @@ Route::group(['middleware' => ['api_db', 'token_auth', 'locale'], 'prefix' => 'a Route::post('emails', 'EmailController@send')->name('email.send')->middleware('user_verified'); Route::resource('expenses', 'ExpenseController'); // name = (expenses. index / create / show / update / destroy / edit - Route::put('expenses/{expense}/upload', 'ExpenseController@upload'); + Route::put('expenses/{expense}/upload', 'ExpenseController@upload'); Route::post('expenses/bulk', 'ExpenseController@bulk')->name('expenses.bulk'); Route::resource('expense_categories', 'ExpenseCategoryController'); // name = (expense_categories. index / create / show / update / destroy / edit @@ -98,7 +98,7 @@ Route::group(['middleware' => ['api_db', 'token_auth', 'locale'], 'prefix' => 'a Route::resource('payments', 'PaymentController'); // name = (payments. index / create / show / update / destroy / edit Route::post('payments/refund', 'PaymentController@refund')->name('payments.refund'); Route::post('payments/bulk', 'PaymentController@bulk')->name('payments.bulk'); - Route::put('payments/{payment}/upload', 'PaymentController@upload'); + Route::put('payments/{payment}/upload', 'PaymentController@upload'); Route::resource('payment_terms', 'PaymentTermController'); // name = (payments. index / create / show / update / destroy / edit Route::post('payment_terms/bulk', 'PaymentTermController@bulk')->name('payment_terms.bulk'); @@ -107,20 +107,20 @@ Route::group(['middleware' => ['api_db', 'token_auth', 'locale'], 'prefix' => 'a Route::resource('products', 'ProductController'); // name = (products. index / create / show / update / destroy / edit Route::post('products/bulk', 'ProductController@bulk')->name('products.bulk'); - Route::put('products/{product}/upload', 'ProductController@upload'); + Route::put('products/{product}/upload', 'ProductController@upload'); Route::resource('projects', 'ProjectController'); // name = (projects. index / create / show / update / destroy / edit Route::post('projects/bulk', 'ProjectController@bulk')->name('projects.bulk'); Route::put('projects/{project}/upload', 'ProjectController@upload')->name('projects.upload'); - + Route::resource('quotes', 'QuoteController'); // name = (quotes. index / create / show / update / destroy / edit Route::get('quotes/{quote}/{action}', 'QuoteController@action')->name('quotes.action'); Route::post('quotes/bulk', 'QuoteController@bulk')->name('quotes.bulk'); - Route::put('quotes/{quote}/upload', 'QuoteController@upload'); + Route::put('quotes/{quote}/upload', 'QuoteController@upload'); Route::resource('recurring_invoices', 'RecurringInvoiceController'); // name = (recurring_invoices. index / create / show / update / destroy / edit Route::post('recurring_invoices/bulk', 'RecurringInvoiceController@bulk')->name('recurring_invoices.bulk'); - Route::put('recurring_invoices/{recurring_invoice}/upload', 'RecurringInvoiceController@upload'); + Route::put('recurring_invoices/{recurring_invoice}/upload', 'RecurringInvoiceController@upload'); Route::resource('recurring_quotes', 'RecurringQuoteController'); // name = (recurring_invoices. index / create / show / update / destroy / edit Route::post('recurring_quotes/bulk', 'RecurringQuoteController@bulk')->name('recurring_quotes.bulk'); @@ -137,7 +137,7 @@ Route::group(['middleware' => ['api_db', 'token_auth', 'locale'], 'prefix' => 'a Route::resource('tasks', 'TaskController'); // name = (tasks. index / create / show / update / destroy / edit Route::post('tasks/bulk', 'TaskController@bulk')->name('tasks.bulk'); - Route::put('tasks/{task}/upload', 'TaskController@upload'); + Route::put('tasks/{task}/upload', 'TaskController@upload'); Route::resource('task_statuses', 'TaskStatusController'); // name = (task_statuses. index / create / show / update / destroy / edit Route::post('task_statuses/bulk', 'TaskStatusController@bulk')->name('task_statuses.bulk'); @@ -155,7 +155,7 @@ Route::group(['middleware' => ['api_db', 'token_auth', 'locale'], 'prefix' => 'a Route::resource('vendors', 'VendorController'); // name = (vendors. index / create / show / update / destroy / edit Route::post('vendors/bulk', 'VendorController@bulk')->name('vendors.bulk'); - Route::put('vendors/{vendor}/upload', 'VendorController@upload'); + Route::put('vendors/{vendor}/upload', 'VendorController@upload'); Route::get('users', 'UserController@index'); Route::put('users/{user}', 'UserController@update')->middleware('password_protected'); @@ -173,7 +173,7 @@ Route::group(['middleware' => ['api_db', 'token_auth', 'locale'], 'prefix' => 'a // Route::post('hooks', 'SubscriptionController@subscribe')->name('hooks.subscribe'); // Route::delete('hooks/{subscription_id}', 'SubscriptionController@unsubscribe')->name('hooks.unsubscribe'); - + Route::resource('billing_subscriptions', 'BillingSubscriptionController'); }); Route::match(['get', 'post'], 'payment_webhook/{company_key}/{company_gateway_id}', 'PaymentWebhookController') diff --git a/tests/Feature/BillingSubscriptionApiTest.php b/tests/Feature/BillingSubscriptionApiTest.php new file mode 100644 index 000000000000..cd66f20f063a --- /dev/null +++ b/tests/Feature/BillingSubscriptionApiTest.php @@ -0,0 +1,134 @@ +makeTestData(); + + Session::start(); + + $this->faker = \Faker\Factory::create(); + + Model::reguard(); + } + + public function testExpenseGet() + { + $product = Product::factory()->create([ + 'company_id' => $this->company->id, + 'user_id' => $this->user->id, + ]); + + $billing_subscription = BillingSubscription::factory()->create([ + 'product_id' => $product->id, + 'company_id' => $this->company->id, + ]); + + $response = $this->withHeaders([ + 'X-API-SECRET' => config('ninja.api_secret'), + 'X-API-TOKEN' => $this->token, + ])->get('/api/v1/billing_subscriptions/' . $this->encodePrimaryKey($billing_subscription->id)); + + $response->assertStatus(200); + } + + public function testBillingSubscriptionsPost() + { + $product = Product::factory()->create([ + 'company_id' => $this->company->id, + 'user_id' => $this->user->id, + ]); + + $response = $this->withHeaders([ + 'X-API-SECRET' => config('ninja.api_secret'), + 'X-API-TOKEN' => $this->token, + ])->post('/api/v1/billing_subscriptions', ['product_id' => $product->id, 'allow_cancellation' => true]); + + $response->assertStatus(200); + } + + public function testBillingSubscriptionPut() + { + $product = Product::factory()->create([ + 'company_id' => $this->company->id, + 'user_id' => $this->user->id, + ]); + + $response1 = $this + ->withHeaders(['X-API-SECRET' => config('ninja.api_secret'),'X-API-TOKEN' => $this->token]) + ->post('/api/v1/billing_subscriptions', ['product_id' => $product->id]) + ->assertStatus(200) + ->json(); + + $response2 = $this + ->withHeaders(['X-API-SECRET' => config('ninja.api_secret'),'X-API-TOKEN' => $this->token]) + ->put('/api/v1/billing_subscriptions/' . $response1['data']['id'], ['allow_cancellation' => true]) + ->assertStatus(200) + ->json(); + + $this->assertNotEquals($response1['data']['allow_cancellation'], $response2['data']['allow_cancellation']); + } + + /* + TypeError : Argument 1 passed to App\Transformers\BillingSubscriptionTransformer::transform() must be an instance of App\Models\BillingSubscription, bool given, called in /var/www/html/vendor/league/fractal/src/Scope.php on line 407 + /var/www/html/app/Transformers/BillingSubscriptionTransformer.php:35 + /var/www/html/vendor/league/fractal/src/Scope.php:407 + /var/www/html/vendor/league/fractal/src/Scope.php:349 + /var/www/html/vendor/league/fractal/src/Scope.php:235 + /var/www/html/app/Http/Controllers/BaseController.php:395 + /var/www/html/app/Http/Controllers/BillingSubscriptionController.php:408 + */ + public function testBillingSubscriptionDeleted() + { + + $product = Product::factory()->create([ + 'company_id' => $this->company->id, + 'user_id' => $this->user->id, + ]); + + $billing_subscription = BillingSubscription::factory()->create([ + 'product_id' => $product->id, + 'company_id' => $this->company->id, + ]); + + $response = $this + ->withHeaders(['X-API-SECRET' => config('ninja.api_secret'), 'X-API-TOKEN' => $this->token]) + ->delete('/api/v1/billing_subscriptions/' . $this->encodePrimaryKey($billing_subscription->id)) + ->assertStatus(200) + ->json(); + + } +}