From 2e139a633bc6115ec65b7d7158d52e4e00ef0d98 Mon Sep 17 00:00:00 2001 From: David Bomba Date: Sun, 28 Mar 2021 08:45:46 +1100 Subject: [PATCH 1/7] Validation rules for subscriptions --- app/Http/Requests/Subscription/StoreSubscriptionRequest.php | 4 ++-- app/Models/Subscription.php | 3 +-- app/PaymentDrivers/BaseDriver.php | 2 +- app/Services/Subscription/SubscriptionService.php | 3 ++- 4 files changed, 6 insertions(+), 6 deletions(-) diff --git a/app/Http/Requests/Subscription/StoreSubscriptionRequest.php b/app/Http/Requests/Subscription/StoreSubscriptionRequest.php index e142d4578f1b..f3a8c280b3e6 100644 --- a/app/Http/Requests/Subscription/StoreSubscriptionRequest.php +++ b/app/Http/Requests/Subscription/StoreSubscriptionRequest.php @@ -13,6 +13,7 @@ namespace App\Http\Requests\Subscription; use App\Http\Requests\Request; use App\Models\Subscription; +use Illuminate\Validation\Rule; class StoreSubscriptionRequest extends Request { @@ -34,10 +35,8 @@ class StoreSubscriptionRequest extends Request 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'], @@ -55,6 +54,7 @@ class StoreSubscriptionRequest extends Request 'plan_map' => ['sometimes'], 'refund_period' => ['sometimes'], 'webhook_configuration' => ['sometimes'], + 'name' => Rule::unique('subscriptions')->where('company_id', auth()->user()->company()->id) ]; } } diff --git a/app/Models/Subscription.php b/app/Models/Subscription.php index 7ee6bff708c7..2fb57e602878 100644 --- a/app/Models/Subscription.php +++ b/app/Models/Subscription.php @@ -21,10 +21,8 @@ class Subscription extends BaseModel use HasFactory, SoftDeletes; protected $fillable = [ - 'user_id', 'product_ids', 'recurring_product_ids', - 'company_id', 'frequency_id', 'auto_bill', 'promo_code', @@ -44,6 +42,7 @@ class Subscription extends BaseModel 'currency_id', 'group_id', 'price', + 'name', ]; protected $casts = [ diff --git a/app/PaymentDrivers/BaseDriver.php b/app/PaymentDrivers/BaseDriver.php index 18f7cb08542e..d3052847aefe 100644 --- a/app/PaymentDrivers/BaseDriver.php +++ b/app/PaymentDrivers/BaseDriver.php @@ -242,7 +242,7 @@ class BaseDriver extends AbstractPaymentDriver event(new PaymentWasCreated($payment, $payment->company, Ninja::eventVars())); if (property_exists($this->payment_hash->data, 'billing_context')) { - $billing_subscription = \App\Models\Subscription::find($this->payment_hash->data->billing_context->billing_subscription_id); + $billing_subscription = \App\Models\Subscription::find($this->payment_hash->data->billing_context->subscription_id); (new SubscriptionService($billing_subscription))->completePurchase($this->payment_hash); } diff --git a/app/Services/Subscription/SubscriptionService.php b/app/Services/Subscription/SubscriptionService.php index 1ab050c39d46..c0a04741214f 100644 --- a/app/Services/Subscription/SubscriptionService.php +++ b/app/Services/Subscription/SubscriptionService.php @@ -99,7 +99,8 @@ class SubscriptionService $invoice = InvoiceFactory::create($this->subscription->company_id, $this->subscription->user_id); $invoice->line_items = $subscription_repo->generateLineItems($this->subscription); - + $invoice->subscription_id = $this->subscription->id; + if(strlen($data['coupon']) >=1 && ($data['coupon'] == $this->subscription->promo_code) && $this->subscription->promo_discount > 0) { $invoice->discount = $subscription->promo_discount; From 6489d73ad6e550d4f556f246d1ba3f0631f08d47 Mon Sep 17 00:00:00 2001 From: David Bomba Date: Sun, 28 Mar 2021 09:15:12 +1100 Subject: [PATCH 2/7] Fixes for subscriptions --- app/Services/Subscription/SubscriptionService.php | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/app/Services/Subscription/SubscriptionService.php b/app/Services/Subscription/SubscriptionService.php index c0a04741214f..f18fa4088e4d 100644 --- a/app/Services/Subscription/SubscriptionService.php +++ b/app/Services/Subscription/SubscriptionService.php @@ -100,11 +100,11 @@ class SubscriptionService $invoice = InvoiceFactory::create($this->subscription->company_id, $this->subscription->user_id); $invoice->line_items = $subscription_repo->generateLineItems($this->subscription); $invoice->subscription_id = $this->subscription->id; - + if(strlen($data['coupon']) >=1 && ($data['coupon'] == $this->subscription->promo_code) && $this->subscription->promo_discount > 0) { - $invoice->discount = $subscription->promo_discount; - $invoice->is_amount_discount = $subscription->is_amount_discount; + $invoice->discount = $this->subscription->promo_discount; + $invoice->is_amount_discount = $this->subscription->is_amount_discount; } return $invoice_repo->save($data, $invoice); From 6e813e233eb993b7368a2f9020126310d801c6f2 Mon Sep 17 00:00:00 2001 From: = Date: Sun, 28 Mar 2021 13:09:47 +1100 Subject: [PATCH 3/7] Fix missing USE in CreditWasViewed event --- app/Events/Credit/CreditWasViewed.php | 1 + 1 file changed, 1 insertion(+) diff --git a/app/Events/Credit/CreditWasViewed.php b/app/Events/Credit/CreditWasViewed.php index fdf9b8708f0e..0bd2405c0d79 100644 --- a/app/Events/Credit/CreditWasViewed.php +++ b/app/Events/Credit/CreditWasViewed.php @@ -12,6 +12,7 @@ namespace App\Events\Credit; use App\Models\Company; +use App\Models\CreditInvitation; use Illuminate\Queue\SerializesModels; /** From 40dc8efd46077f0dc7175b98b4feb8269f8463c0 Mon Sep 17 00:00:00 2001 From: = Date: Sun, 28 Mar 2021 13:19:44 +1100 Subject: [PATCH 4/7] limit logging conditionally --- app/Exceptions/Handler.php | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/app/Exceptions/Handler.php b/app/Exceptions/Handler.php index 3351dc65a4e1..623d33bbd4c2 100644 --- a/app/Exceptions/Handler.php +++ b/app/Exceptions/Handler.php @@ -94,7 +94,9 @@ class Handler extends ExceptionHandler } } - parent::report($exception); + if(config('ninja.expanded_logging')) + parent::report($exception); + } private function validException($exception) @@ -105,7 +107,7 @@ class Handler extends ExceptionHandler if (strpos($exception->getMessage(), 'Permission denied') !== false) return false; - if (strpos($exception->getMessage(), 'flock()') !== false) + if (strpos($exception->getMessage(), 'flock') !== false) return false; if (strpos($exception->getMessage(), 'expects parameter 1 to be resource') !== false) @@ -114,6 +116,8 @@ class Handler extends ExceptionHandler if (strpos($exception->getMessage(), 'fwrite()') !== false) return false; + if(strpos($exception->getMessage()), 'LockableFile' !== false) + return false; return true; } From 646130a10453e3aad5efd2bf1a7d30e4a6f18b78 Mon Sep 17 00:00:00 2001 From: = Date: Sun, 28 Mar 2021 13:26:46 +1100 Subject: [PATCH 5/7] Fixes for tests --- app/Exceptions/Handler.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/Exceptions/Handler.php b/app/Exceptions/Handler.php index 623d33bbd4c2..9ecf8470c940 100644 --- a/app/Exceptions/Handler.php +++ b/app/Exceptions/Handler.php @@ -96,7 +96,7 @@ class Handler extends ExceptionHandler if(config('ninja.expanded_logging')) parent::report($exception); - + } private function validException($exception) @@ -116,7 +116,7 @@ class Handler extends ExceptionHandler if (strpos($exception->getMessage(), 'fwrite()') !== false) return false; - if(strpos($exception->getMessage()), 'LockableFile' !== false) + if(strpos($exception->getMessage(), 'LockableFile') !== false) return false; return true; From d7bf92754953a2825f7eb0a74b4b1bf8ca36c912 Mon Sep 17 00:00:00 2001 From: David Bomba Date: Mon, 29 Mar 2021 13:14:55 +1100 Subject: [PATCH 6/7] Set exchange rates on invoices --- app/Http/Controllers/BaseController.php | 8 ++++ app/Repositories/PaymentRepository.php | 1 - .../RecurringInvoiceRepository.php | 12 ----- app/Repositories/SubscriptionRepository.php | 13 +++--- app/Services/Invoice/InvoiceService.php | 17 ++++++- .../Subscription/SubscriptionService.php | 29 +++++++----- app/Transformers/CompanyTransformer.php | 4 +- app/Transformers/SubscriptionTransformer.php | 4 +- tests/Unit/RecurringDateTest.php | 44 +++++++++++++++++++ 9 files changed, 97 insertions(+), 35 deletions(-) create mode 100644 tests/Unit/RecurringDateTest.php diff --git a/app/Http/Controllers/BaseController.php b/app/Http/Controllers/BaseController.php index 4fa44c019a2a..19b5c02d6f50 100644 --- a/app/Http/Controllers/BaseController.php +++ b/app/Http/Controllers/BaseController.php @@ -92,6 +92,7 @@ class BaseController extends Controller 'company.quotes.invitations.company', 'company.quotes.documents', 'company.tasks.documents', + 'company.subcsriptions', 'company.tax_rates', 'company.tokens_hashed', 'company.vendors.contacts.company', @@ -340,6 +341,13 @@ class BaseController extends Controller if(!$user->isAdmin()) $query->where('activities.user_id', $user->id); + }, + 'company.subscriptions'=> function ($query) use($user) { + $query->where('updated_at', '>=', $updated_at); + + if(!$user->isAdmin()) + $query->where('subscriptions.user_id', $user->id); + } ] ); diff --git a/app/Repositories/PaymentRepository.php b/app/Repositories/PaymentRepository.php index 1bd177c7fd56..95e3c4ba4c91 100644 --- a/app/Repositories/PaymentRepository.php +++ b/app/Repositories/PaymentRepository.php @@ -182,7 +182,6 @@ class PaymentRepository extends BaseRepository { $company_currency = $client->company->settings->currency_id; if ($company_currency != $client_currency) { - $currency = $client->currency(); $exchange_rate = new CurrencyApi(); diff --git a/app/Repositories/RecurringInvoiceRepository.php b/app/Repositories/RecurringInvoiceRepository.php index b74560bc1b1e..96b37281bef6 100644 --- a/app/Repositories/RecurringInvoiceRepository.php +++ b/app/Repositories/RecurringInvoiceRepository.php @@ -24,18 +24,6 @@ class RecurringInvoiceRepository extends BaseRepository { $invoice = $this->alternativeSave($data, $invoice); - // $invoice->fill($data); - - // $invoice->save(); - - // $invoice_calc = new InvoiceSum($invoice); - - // $invoice->service() - // ->applyNumber() - // ->createInvitations() - // ->save(); - - // $invoice = $invoice_calc->build()->getRecurringInvoice(); return $invoice; } diff --git a/app/Repositories/SubscriptionRepository.php b/app/Repositories/SubscriptionRepository.php index 32ce6a931bd8..466857f4a28e 100644 --- a/app/Repositories/SubscriptionRepository.php +++ b/app/Repositories/SubscriptionRepository.php @@ -93,17 +93,20 @@ class SubscriptionRepository extends BaseRepository return $data; } - public function generateLineItems($subscription) + public function generateLineItems($subscription, $is_recurring = false) { $line_items = []; - foreach($subscription->service()->products() as $product) + if(!$is_recurring) { - $line_items[] = (array)$this->makeLineItem($product); + foreach($subscription->service()->products() as $product) + { + $line_items[] = (array)$this->makeLineItem($product); + } } - - foreach($subscription->service()->recurring_products() as $product) + + foreach($subscription->service()->recurringProducts() as $product) { $line_items[] = (array)$this->makeLineItem($product); } diff --git a/app/Services/Invoice/InvoiceService.php b/app/Services/Invoice/InvoiceService.php index 83298578b382..72e47ceecd49 100644 --- a/app/Services/Invoice/InvoiceService.php +++ b/app/Services/Invoice/InvoiceService.php @@ -63,11 +63,22 @@ class InvoiceService return $this; } + /** + * Sets the exchange rate on the invoice if the client currency + * is different to the company currency. + */ public function setExchangeRate() { - $exchange_rate = new CurrencyApi(); - // $payment->exchange_rate = $exchange_rate->exchangeRate($client_currency, $company_currency, Carbon::parse($payment->date)); + $client_currency = $this->invoice->client->getSetting('currency_id'); + $company_currency = $this->invoice->company->settings->currency_id; + + if ($company_currency != $client_currency) { + + $exchange_rate = new CurrencyApi(); + + $this->invoice->exchange_rate = $exchange_rate->exchangeRate($client_currency, $company_currency, now()); + } return $this; } @@ -140,6 +151,8 @@ class InvoiceService { $this->invoice = (new MarkSent($this->invoice->client, $this->invoice))->run(); + $this->setExchangeRate(); + return $this; } diff --git a/app/Services/Subscription/SubscriptionService.php b/app/Services/Subscription/SubscriptionService.php index f18fa4088e4d..e82067ead335 100644 --- a/app/Services/Subscription/SubscriptionService.php +++ b/app/Services/Subscription/SubscriptionService.php @@ -14,6 +14,7 @@ namespace App\Services\Subscription; use App\DataMapper\InvoiceItem; use App\Factory\InvoiceFactory; use App\Factory\InvoiceToRecurringInvoiceFactory; +use App\Factory\RecurringInvoiceFactory; use App\Jobs\Util\SystemLogger; use App\Models\ClientContact; use App\Models\ClientSubscription; @@ -23,6 +24,7 @@ use App\Models\Product; use App\Models\Subscription; use App\Models\SystemLog; use App\Repositories\InvoiceRepository; +use App\Repositories\RecurringInvoiceRepository; use App\Repositories\SubscriptionRepository; use App\Utils\Traits\CleanLineItems; use App\Utils\Traits\MakesHash; @@ -69,18 +71,23 @@ class SubscriptionService if(!$this->subscription->trial_enabled) return new \Exception("Trials are disabled for this product"); - // $contact = ClientContact::with('client')->find($data['contact_id']); + //create recurring invoice with start date = trial_duration + 1 day + $recurring_invoice_repo = new RecurringInvoiceRepository(); + $subscription_repo = new SubscriptionRepository(); - // $cs = new ClientSubscription(); - // $cs->subscription_id = $this->subscription->id; - // $cs->company_id = $this->subscription->company_id; - // $cs->trial_started = time(); - // $cs->trial_ends = time() + $this->subscription->trial_duration; - // $cs->quantity = $data['quantity']; - // $cs->client_id = $contact->client->id; - // $cs->save(); + $invoice = RecurringInvoiceFactory::create($this->subscription->company_id, $this->subscription->user_id); + $invoice->line_items = $subscription_repo->generateLineItems($this->subscription, true); + $invoice->subscription_id = $this->subscription->id; + $invoice->frequency_id = $this->subscription->frequency_id; + $invoice->date = now()->addSeconds($this->subscription->trial_duration)->addDays(1); - // $this->client_subscription = $cs; + if(strlen($data['coupon']) >=1 && ($data['coupon'] == $this->subscription->promo_code) && $this->subscription->promo_discount > 0) + { + $invoice->discount = $this->subscription->promo_discount; + $invoice->is_amount_discount = $this->subscription->is_amount_discount; + } + + $recurring_invoice = $recurring_invoice_repo->save($data, $invoice); //execute any webhooks $this->triggerWebhook(); @@ -198,7 +205,7 @@ class SubscriptionService return Product::whereIn('id', $this->transformKeys(explode(",", $this->subscription->product_ids)))->get(); } - public function recurring_products() + public function recurringProducts() { return Product::whereIn('id', $this->transformKeys(explode(",", $this->subscription->recurring_product_ids)))->get(); } diff --git a/app/Transformers/CompanyTransformer.php b/app/Transformers/CompanyTransformer.php index 5fd5857f5973..2541dfd0f90d 100644 --- a/app/Transformers/CompanyTransformer.php +++ b/app/Transformers/CompanyTransformer.php @@ -92,7 +92,7 @@ class CompanyTransformer extends EntityTransformer 'system_logs', 'expense_categories', 'task_statuses', - 'billing_subscriptions', + 'subscriptions', ]; /** @@ -366,6 +366,6 @@ class CompanyTransformer extends EntityTransformer { $transformer = new SubscriptionTransformer($this->serializer); - return $this->includeCollection($company->billing_subscriptions, $transformer, Subscription::class); + return $this->includeCollection($company->subscriptions, $transformer, Subscription::class); } } diff --git a/app/Transformers/SubscriptionTransformer.php b/app/Transformers/SubscriptionTransformer.php index da8b892d320c..70f7eb5cfd5d 100644 --- a/app/Transformers/SubscriptionTransformer.php +++ b/app/Transformers/SubscriptionTransformer.php @@ -39,8 +39,8 @@ class SubscriptionTransformer extends EntityTransformer 'id' => $this->encodePrimaryKey($subscription->id), 'user_id' => $this->encodePrimaryKey($subscription->user_id), 'group_id' => $this->encodePrimaryKey($subscription->group_id), - 'product_ids' => $subscription->product_ids, - 'recurring_product_ids' => $subscription->recurring_product_ids, + 'product_ids' => (string)$subscription->product_ids, + 'recurring_product_ids' => (string)$subscription->recurring_product_ids, 'assigned_user_id' => $this->encodePrimaryKey($subscription->assigned_user_id), 'company_id' => $this->encodePrimaryKey($subscription->company_id), 'price' => (float) $subscription->price, diff --git a/tests/Unit/RecurringDateTest.php b/tests/Unit/RecurringDateTest.php new file mode 100644 index 000000000000..db393f928bd3 --- /dev/null +++ b/tests/Unit/RecurringDateTest.php @@ -0,0 +1,44 @@ +makeTestData(); + } + + public function testNextDay() + { + $trial = 60*60*24; + + $now = Carbon::parse('2021-12-01'); + + $trial_ends = $now->addSeconds($trial)->addDays(1); + + $this->assertequals($trial_ends->format('Y-m-d'), '2021-12-03'); + } +} \ No newline at end of file From db7df69db797cfca43808a15e8f252a80cfe9b5c Mon Sep 17 00:00:00 2001 From: David Bomba Date: Mon, 29 Mar 2021 14:49:29 +1100 Subject: [PATCH 7/7] Recurring services - trial phase' --- app/Http/Controllers/BaseController.php | 2 +- .../Subscription/SubscriptionService.php | 21 ++++++++++++------- 2 files changed, 15 insertions(+), 8 deletions(-) diff --git a/app/Http/Controllers/BaseController.php b/app/Http/Controllers/BaseController.php index 19b5c02d6f50..891eeec07bd3 100644 --- a/app/Http/Controllers/BaseController.php +++ b/app/Http/Controllers/BaseController.php @@ -342,7 +342,7 @@ class BaseController extends Controller $query->where('activities.user_id', $user->id); }, - 'company.subscriptions'=> function ($query) use($user) { + 'company.subscriptions'=> function ($query) use($updated_at, $user) { $query->where('updated_at', '>=', $updated_at); if(!$user->isAdmin()) diff --git a/app/Services/Subscription/SubscriptionService.php b/app/Services/Subscription/SubscriptionService.php index e82067ead335..512356906a6e 100644 --- a/app/Services/Subscription/SubscriptionService.php +++ b/app/Services/Subscription/SubscriptionService.php @@ -75,19 +75,26 @@ class SubscriptionService $recurring_invoice_repo = new RecurringInvoiceRepository(); $subscription_repo = new SubscriptionRepository(); - $invoice = RecurringInvoiceFactory::create($this->subscription->company_id, $this->subscription->user_id); - $invoice->line_items = $subscription_repo->generateLineItems($this->subscription, true); - $invoice->subscription_id = $this->subscription->id; - $invoice->frequency_id = $this->subscription->frequency_id; - $invoice->date = now()->addSeconds($this->subscription->trial_duration)->addDays(1); + $recurring_invoice = RecurringInvoiceFactory::create($this->subscription->company_id, $this->subscription->user_id); + $recurring_invoice->line_items = $subscription_repo->generateLineItems($this->subscription, true); + $recurring_invoice->subscription_id = $this->subscription->id; + $recurring_invoice->frequency_id = $this->subscription->frequency_id; + $recurring_invoice->date = now(); + $recurring_invoice->next_send_date = now()->addSeconds($this->subscription->trial_duration)->addDays(1); + $recurring_invoice->remaining_cycles = -1; if(strlen($data['coupon']) >=1 && ($data['coupon'] == $this->subscription->promo_code) && $this->subscription->promo_discount > 0) { - $invoice->discount = $this->subscription->promo_discount; - $invoice->is_amount_discount = $this->subscription->is_amount_discount; + $recurring_invoice->discount = $this->subscription->promo_discount; + $recurring_invoice->is_amount_discount = $this->subscription->is_amount_discount; } $recurring_invoice = $recurring_invoice_repo->save($data, $invoice); + + /* Start the recurring service */ + $recurring_invoice->service() + ->start() + ->save(); //execute any webhooks $this->triggerWebhook();