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..38708bd6ed79 --- /dev/null +++ b/app/Factory/BillingSubscriptionFactory.php @@ -0,0 +1,17 @@ +billing_subscription_repo = $billing_subscription_repo; + } + + public function index(): \Illuminate\Http\Response + { + $billing_subscriptions = BillingSubscription::query()->company(); + + return $this->listResponse($billing_subscriptions); + } + + public function create(CreateBillingSubscriptionRequest $request): \Illuminate\Http\Response + { + $billing_subscription = BillingSubscriptionFactory::create(auth()->user()->company()->id, auth()->user()->id); + + return $this->itemResponse($billing_subscription); + } + + 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); + } + + public function show(ShowBillingSubscriptionRequest $request, BillingSubscription $billing_subscription): \Illuminate\Http\Response + { + return $this->itemResponse($billing_subscription); + } + + public function edit(EditBillingSubscriptionRequest $request, BillingSubscription $billing_subscription): \Illuminate\Http\Response + { + return $this->itemResponse($billing_subscription); + } + + 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); + } + + public function destroy(DestroyBillingSubscriptionRequest $request, BillingSubscription $billing_subscription): \Illuminate\Http\Response + { + $this->billing_subscription_repo->delete($billing_subscription); + + return $this->listResponse($billing_subscription->fresh()); + } +} diff --git a/app/Http/Requests/BillingSubscription/CreateBillingSubscriptionRequest.php b/app/Http/Requests/BillingSubscription/CreateBillingSubscriptionRequest.php new file mode 100644 index 000000000000..f7f0a907343d --- /dev/null +++ b/app/Http/Requests/BillingSubscription/CreateBillingSubscriptionRequest.php @@ -0,0 +1,31 @@ +user()->can('create', BillingSubscription::class); // TODO + } + + /** + * 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..c263836d9442 --- /dev/null +++ b/app/Http/Requests/BillingSubscription/DestroyBillingSubscriptionRequest.php @@ -0,0 +1,31 @@ +user()->can('view', $this->billing_subscription); // TODO + } + + /** + * 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..f5d08bec9445 --- /dev/null +++ b/app/Http/Requests/BillingSubscription/ShowBillingSubscriptionRequest.php @@ -0,0 +1,32 @@ +user()->can('view', $this->billing_subscription); // TODO + } + + /** + * 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..c897edc5b299 --- /dev/null +++ b/app/Http/Requests/BillingSubscription/StoreBillingSubscriptionRequest.php @@ -0,0 +1,50 @@ + ['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..a92d14ff0b33 --- /dev/null +++ b/app/Http/Requests/BillingSubscription/UpdateBillingSubscriptionRequest.php @@ -0,0 +1,33 @@ +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..8ca8caa87d37 --- /dev/null +++ b/app/Models/ClientSubscription.php @@ -0,0 +1,11 @@ +fill($data) + ->save(); + + return $billing_subscription; + } +} diff --git a/app/Transformers/BillingSubscriptionTransformer.php b/app/Transformers/BillingSubscriptionTransformer.php new file mode 100644 index 000000000000..4969f4dfa018 --- /dev/null +++ b/app/Transformers/BillingSubscriptionTransformer.php @@ -0,0 +1,70 @@ + $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' => (string)$billing_subscription->promo_discount, + 'is_amount_discount' => (bool)$billing_subscription->is_amount_discount, + 'allow_cancellation' => (bool)$billing_subscription->allow_cancellation, + 'per_set_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' => (string)$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' => (string)$billing_subscription->refund_period, + 'webhook_configuration' => (string)$billing_subscription->webhook_configuration, + 'is_deleted' => (bool)$billing_subscription->is_deleted, + ]; + } + + public function includeProducts(BillingSubscription $billing_subscription): \League\Fractal\Resource\Item + { + $transformer = new ProductTransformer($this->serializer); + + return $this->includeItem($billing_subscription->product, $transformer, Product::class); + } +} diff --git a/database/migrations/2021_03_08_123729_create_billing_subscriptions_table.php b/database/migrations/2021_03_08_123729_create_billing_subscriptions_table.php new file mode 100644 index 000000000000..640a21becc0c --- /dev/null +++ b/database/migrations/2021_03_08_123729_create_billing_subscriptions_table.php @@ -0,0 +1,71 @@ +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_set_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/routes/api.php b/routes/api.php index a503f03d9765..a2122998f186 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')