diff --git a/app/Http/Requests/Credit/UpdateCreditRequest.php b/app/Http/Requests/Credit/UpdateCreditRequest.php index 2c7c43d39bf9..617321a9ce89 100644 --- a/app/Http/Requests/Credit/UpdateCreditRequest.php +++ b/app/Http/Requests/Credit/UpdateCreditRequest.php @@ -67,8 +67,10 @@ class UpdateCreditRequest extends Request $input = $this->decodePrimaryKeys($input); - $input['line_items'] = isset($input['line_items']) ? $this->cleanItems($input['line_items']) : []; - + if (isset($input['line_items'])) { + $input['line_items'] = isset($input['line_items']) ? $this->cleanItems($input['line_items']) : []; + } + $input['id'] = $this->credit->id; $this->replace($input); diff --git a/app/Http/Requests/Invoice/UpdateInvoiceRequest.php b/app/Http/Requests/Invoice/UpdateInvoiceRequest.php index 0fb69a64e3f6..ec08c200bb90 100644 --- a/app/Http/Requests/Invoice/UpdateInvoiceRequest.php +++ b/app/Http/Requests/Invoice/UpdateInvoiceRequest.php @@ -66,8 +66,10 @@ class UpdateInvoiceRequest extends Request $input['id'] = $this->invoice->id; - $input['line_items'] = isset($input['line_items']) ? $this->cleanItems($input['line_items']) : []; - + if (isset($input['line_items'])) { + $input['line_items'] = isset($input['line_items']) ? $this->cleanItems($input['line_items']) : []; + } + if (array_key_exists('documents', $input)) { unset($input['documents']); } diff --git a/app/Jobs/RecurringInvoice/SendRecurring.php b/app/Jobs/RecurringInvoice/SendRecurring.php index 8dac7d6cfc5f..39712889fc3b 100644 --- a/app/Jobs/RecurringInvoice/SendRecurring.php +++ b/app/Jobs/RecurringInvoice/SendRecurring.php @@ -64,6 +64,7 @@ class SendRecurring implements ShouldQueue ->applyNumber() ->createInvitations() ->fillDefaults() + ->setExchangeRate() ->save(); nlog("Invoice {$invoice->number} created"); diff --git a/app/Models/Presenters/CompanyPresenter.php b/app/Models/Presenters/CompanyPresenter.php index 8baaf73751a2..d7cb21a24b56 100644 --- a/app/Models/Presenters/CompanyPresenter.php +++ b/app/Models/Presenters/CompanyPresenter.php @@ -97,13 +97,13 @@ class CompanyPresenter extends EntityPresenter } } - public function getSpcQrCode($client_currency, $invoice_number, $balance_due_raw) + public function getSpcQrCode($client_currency, $invoice_number, $balance_due_raw, $user_iban) { $settings = $this->entity->settings; return - "SPC\n0200\n1\nCH860021421411198240K\nK\n{$this->name}\n{$settings->address1}\n{$settings->postal_code} {$settings->city}\n\n\nCH\n\n\n\n\n\n\n\n{$balance_due_raw}\n{$client_currency}\n\n\n\n\n\n\n\nNON\n\n{$invoice_number}\nEPD\n"; + "SPC\n0200\n1\n{$user_iban}\nK\n{$this->name}\n{$settings->address1}\n{$settings->postal_code} {$settings->city}\n\n\nCH\n\n\n\n\n\n\n\n{$balance_due_raw}\n{$client_currency}\n\n\n\n\n\n\n\nNON\n\n{$invoice_number}\nEPD\n"; } } diff --git a/app/Models/Subscription.php b/app/Models/Subscription.php index 3bed8c2fe2e9..7ee6bff708c7 100644 --- a/app/Models/Subscription.php +++ b/app/Models/Subscription.php @@ -43,6 +43,7 @@ class Subscription extends BaseModel 'webhook_configuration', 'currency_id', 'group_id', + 'price', ]; protected $casts = [ diff --git a/app/Repositories/SubscriptionRepository.php b/app/Repositories/SubscriptionRepository.php index 4a7a5469facc..32ce6a931bd8 100644 --- a/app/Repositories/SubscriptionRepository.php +++ b/app/Repositories/SubscriptionRepository.php @@ -13,16 +13,125 @@ namespace App\Repositories; +use App\DataMapper\InvoiceItem; +use App\Factory\InvoiceFactory; +use App\Models\Client; +use App\Models\ClientContact; +use App\Models\Invoice; +use App\Models\InvoiceInvitation; use App\Models\Subscription; +use App\Utils\Traits\CleanLineItems; +use Illuminate\Support\Facades\DB; class SubscriptionRepository extends BaseRepository { + use CleanLineItems; + public function save($data, Subscription $subscription): ?Subscription { - $subscription - ->fill($data) - ->save(); + $subscription->fill($data); + + $calculated_prices = $this->calculatePrice($subscription); + + $subscription->price = $calculated_prices['price']; + $subscription->promo_price = $calculated_prices['promo_price']; + + $subscription->save(); return $subscription; } + + private function calculatePrice($subscription) :array + { + + DB::beginTransaction(); + + $data = []; + + $client = Client::factory()->create([ + 'user_id' => $subscription->user_id, + 'company_id' => $subscription->company_id, + 'group_settings_id' => $subscription->group_id, + 'country_id' => $subscription->company->settings->country_id, + ]); + + $contact = ClientContact::factory()->create([ + 'user_id' => $subscription->user_id, + 'company_id' => $subscription->company_id, + 'client_id' => $client->id, + 'is_primary' => 1, + 'send_email' => true, + ]); + + $invoice = InvoiceFactory::create($subscription->company_id, $subscription->user_id); + $invoice->client_id = $client->id; + + $invoice->save(); + + $invitation = InvoiceInvitation::factory()->create([ + 'user_id' => $subscription->user_id, + 'company_id' => $subscription->company_id, + 'invoice_id' => $invoice->id, + 'client_contact_id' => $contact->id, + ]); + + $invoice->setRelation('invitations', $invitation); + $invoice->setRelation('client', $client); + $invoice->setRelation('company', $subscription->company); + $invoice->load('client'); + $invoice->line_items = $this->generateLineItems($subscription); + + $data['price'] = $invoice->calc()->getTotal(); + + $invoice->discount = $subscription->promo_discount; + $invoice->is_amount_discount = $subscription->is_amount_discount; + + $data['promo_price'] = $invoice->calc()->getTotal(); + + DB::rollBack(); + + return $data; + } + + public function generateLineItems($subscription) + { + + $line_items = []; + + foreach($subscription->service()->products() as $product) + { + $line_items[] = (array)$this->makeLineItem($product); + } + + foreach($subscription->service()->recurring_products() as $product) + { + $line_items[] = (array)$this->makeLineItem($product); + } + + $line_items = $this->cleanItems($line_items); + + return $line_items; + + } + + private function makeLineItem($product) + { + $item = new InvoiceItem; + $item->quantity = $product->quantity; + $item->product_key = $product->product_key; + $item->notes = $product->notes; + $item->cost = $product->price; + $item->tax_rate1 = $product->tax_rate1 ?: 0; + $item->tax_name1 = $product->tax_name1 ?: ''; + $item->tax_rate2 = $product->tax_rate2 ?: 0; + $item->tax_name2 = $product->tax_name2 ?: ''; + $item->tax_rate3 = $product->tax_rate3 ?: 0; + $item->tax_name3 = $product->tax_name3 ?: ''; + $item->custom_value1 = $product->custom_value1 ?: ''; + $item->custom_value2 = $product->custom_value2 ?: ''; + $item->custom_value3 = $product->custom_value3 ?: ''; + $item->custom_value4 = $product->custom_value4 ?: ''; + + return $item; + } } \ No newline at end of file diff --git a/app/Services/Invoice/InvoiceService.php b/app/Services/Invoice/InvoiceService.php index 755291c8e572..83298578b382 100644 --- a/app/Services/Invoice/InvoiceService.php +++ b/app/Services/Invoice/InvoiceService.php @@ -14,6 +14,7 @@ namespace App\Services\Invoice; use App\Jobs\Entity\CreateEntityPdf; use App\Jobs\Invoice\InvoiceWorkflowSettings; use App\Jobs\Util\UnlinkFile; +use App\Libraries\Currency\Conversion\CurrencyApi; use App\Models\CompanyGateway; use App\Models\Expense; use App\Models\Invoice; @@ -62,7 +63,14 @@ class InvoiceService return $this; } + public function setExchangeRate() + { + $exchange_rate = new CurrencyApi(); + // $payment->exchange_rate = $exchange_rate->exchangeRate($client_currency, $company_currency, Carbon::parse($payment->date)); + + return $this; + } /** * Applies the recurring invoice number. * @return $this InvoiceService object diff --git a/app/Services/Subscription/SubscriptionService.php b/app/Services/Subscription/SubscriptionService.php index d936b16c7473..1ab050c39d46 100644 --- a/app/Services/Subscription/SubscriptionService.php +++ b/app/Services/Subscription/SubscriptionService.php @@ -15,14 +15,15 @@ use App\DataMapper\InvoiceItem; use App\Factory\InvoiceFactory; use App\Factory\InvoiceToRecurringInvoiceFactory; use App\Jobs\Util\SystemLogger; -use App\Models\Subscription; use App\Models\ClientContact; use App\Models\ClientSubscription; use App\Models\Invoice; use App\Models\PaymentHash; use App\Models\Product; +use App\Models\Subscription; use App\Models\SystemLog; use App\Repositories\InvoiceRepository; +use App\Repositories\SubscriptionRepository; use App\Utils\Traits\CleanLineItems; use App\Utils\Traits\MakesHash; use GuzzleHttp\RequestOptions; @@ -36,7 +37,7 @@ class SubscriptionService private $subscription; /** @var client_subscription */ - private $client_subscription; + // private $client_subscription; public function __construct(Subscription $subscription) { @@ -50,13 +51,8 @@ class SubscriptionService throw new \Exception("Illegal entrypoint into method, payload must contain billing context"); } - // At this point we have some state carried from the billing page - // to this, available as $payment_hash->data->billing_context. Make something awesome ⭐ - - // create client subscription record - // - // create recurring invoice if is_recurring - // + // if we have a recurring product - then generate a recurring invoice + // if trial is enabled, generate the recurring invoice to fire when the trial ends. } @@ -73,18 +69,18 @@ class SubscriptionService if(!$this->subscription->trial_enabled) return new \Exception("Trials are disabled for this product"); - $contact = ClientContact::with('client')->find($data['contact_id']); + // $contact = ClientContact::with('client')->find($data['contact_id']); - $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(); + // $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(); - $this->client_subscription = $cs; + // $this->client_subscription = $cs; //execute any webhooks $this->triggerWebhook(); @@ -99,89 +95,21 @@ class SubscriptionService { $invoice_repo = new InvoiceRepository(); + $subscription_repo = new SubscriptionRepository(); - $data['line_items'] = $this->cleanItems($this->createLineItems($data)); + $invoice = InvoiceFactory::create($this->subscription->company_id, $this->subscription->user_id); + $invoice->line_items = $subscription_repo->generateLineItems($this->subscription); - return $invoice_repo->save($data, InvoiceFactory::create($this->subscription->company_id, $this->subscription->user_id)); - - } - - /** - * Creates the required line items for the invoice - * for the billing subscription. - */ - private function createLineItems($data): array - { - - $line_items = []; - - $product = $this->subscription->product; - - $item = new InvoiceItem; - $item->quantity = $data['quantity']; - $item->product_key = $product->product_key; - $item->notes = $product->notes; - $item->cost = $product->price; - $item->tax_rate1 = $product->tax_rate1 ?: 0; - $item->tax_name1 = $product->tax_name1 ?: ''; - $item->tax_rate2 = $product->tax_rate2 ?: 0; - $item->tax_name2 = $product->tax_name2 ?: ''; - $item->tax_rate3 = $product->tax_rate3 ?: 0; - $item->tax_name3 = $product->tax_name3 ?: ''; - $item->custom_value1 = $product->custom_value1 ?: ''; - $item->custom_value2 = $product->custom_value2 ?: ''; - $item->custom_value3 = $product->custom_value3 ?: ''; - $item->custom_value4 = $product->custom_value4 ?: ''; - - //$item->type_id need to switch whether the subscription is a service or product - - $line_items[] = $item; - - - //do we have a promocode? enter this as a line item. if(strlen($data['coupon']) >=1 && ($data['coupon'] == $this->subscription->promo_code) && $this->subscription->promo_discount > 0) - $line_items[] = $this->createPromoLine($data); + { + $invoice->discount = $subscription->promo_discount; + $invoice->is_amount_discount = $subscription->is_amount_discount; + } - return $line_items; + return $invoice_repo->save($data, $invoice); } - /** - * If a coupon is entered (and is valid) - * then we apply the coupon discount with a line item. - */ - private function createPromoLine($data) - { - - $product = $this->subscription->product; - $discounted_amount = 0; - $discount = 0; - $amount = $data['quantity'] * $product->cost; - - if ($this->subscription->is_amount_discount == true) { - $discount = $this->subscription->promo_discount; - } - else { - $discount = round($amount * ($this->subscription->promo_discount / 100), 2); - } - - $discounted_amount = $amount - $discount; - - $item = new InvoiceItem; - $item->quantity = 1; - $item->product_key = ctrans('texts.promo_code'); - $item->notes = ctrans('texts.promo_code'); - $item->cost = $discounted_amount; - $item->tax_rate1 = $product->tax_rate1 ?: 0; - $item->tax_name1 = $product->tax_name1 ?: ''; - $item->tax_rate2 = $product->tax_rate2 ?: 0; - $item->tax_name2 = $product->tax_name2 ?: ''; - $item->tax_rate3 = $product->tax_rate3 ?: 0; - $item->tax_name3 = $product->tax_name3 ?: ''; - - return $item; - - } private function convertInvoiceToRecurring($payment_hash) { @@ -190,71 +118,72 @@ class SubscriptionService if(!$invoice) throw new \Exception("Could not match an invoice for payment of billing subscription"); - - //todo - need to remove the promo code - if it exists return InvoiceToRecurringInvoiceFactory::create($invoice); } - public function createClientSubscription($payment_hash) - { + // @deprecated due to change in architecture - //is this a recurring or one off subscription. + // public function createClientSubscription($payment_hash) + // { - $cs = new ClientSubscription(); - $cs->subscription_id = $this->subscription->id; - $cs->company_id = $this->subscription->company_id; + // //is this a recurring or one off subscription. - $cs->invoice_id = $payment_hash->billing_context->invoice_id; - $cs->client_id = $payment_hash->billing_context->client_id; - $cs->quantity = $payment_hash->billing_context->quantity; + // $cs = new ClientSubscription(); + // $cs->subscription_id = $this->subscription->id; + // $cs->company_id = $this->subscription->company_id; - //if is_recurring - //create recurring invoice from invoice - if($this->subscription->is_recurring) - { - $recurring_invoice = $this->convertInvoiceToRecurring($payment_hash); - $recurring_invoice->frequency_id = $this->subscription->frequency_id; - $recurring_invoice->next_send_date = $recurring_invoice->nextDateByFrequency(now()->format('Y-m-d')); - $recurring_invoice->save(); - $cs->recurring_invoice_id = $recurring_invoice->id; + // $cs->invoice_id = $payment_hash->billing_context->invoice_id; + // $cs->client_id = $payment_hash->billing_context->client_id; + // $cs->quantity = $payment_hash->billing_context->quantity; - //?set the recurring invoice as active - set the date here also based on the frequency? - $recurring_invoice->service()->start(); - } + // //if is_recurring + // //create recurring invoice from invoice + // if($this->subscription->is_recurring) + // { + // $recurring_invoice = $this->convertInvoiceToRecurring($payment_hash); + // $recurring_invoice->frequency_id = $this->subscription->frequency_id; + // $recurring_invoice->next_send_date = $recurring_invoice->nextDateByFrequency(now()->format('Y-m-d')); + // $recurring_invoice->save(); + // $cs->recurring_invoice_id = $recurring_invoice->id; + + // //?set the recurring invoice as active - set the date here also based on the frequency? + // $recurring_invoice->service()->start(); + // } - $cs->save(); + // $cs->save(); - $this->client_subscription = $cs; + // $this->client_subscription = $cs; - } + // } + //@todo - need refactor public function triggerWebhook() { //hit the webhook to after a successful onboarding - $body = [ - 'subscription' => $this->subscription, - 'client_subscription' => $this->client_subscription, - 'client' => $this->client_subscription->client->toArray(), - ]; + // $body = [ + // 'subscription' => $this->subscription, + // 'client_subscription' => $this->client_subscription, + // 'client' => $this->client_subscription->client->toArray(), + // ]; - $client = new \GuzzleHttp\Client(['headers' => $this->subscription->webhook_configuration->post_purchase_headers]); + // $client = new \GuzzleHttp\Client(['headers' => $this->subscription->webhook_configuration->post_purchase_headers]); - $response = $client->{$this->subscription->webhook_configuration->post_purchase_rest_method}($this->subscription->post_purchase_url,[ - RequestOptions::JSON => ['body' => $body] - ]); + // $response = $client->{$this->subscription->webhook_configuration->post_purchase_rest_method}($this->subscription->post_purchase_url,[ + // RequestOptions::JSON => ['body' => $body] + // ]); - SystemLogger::dispatch( - $body, - SystemLog::CATEGORY_WEBHOOK, - SystemLog::EVENT_WEBHOOK_RESPONSE, - SystemLog::TYPE_WEBHOOK_RESPONSE, - $this->client_subscription->client, - ); + // SystemLogger::dispatch( + // $body, + // SystemLog::CATEGORY_WEBHOOK, + // SystemLog::EVENT_WEBHOOK_RESPONSE, + // SystemLog::TYPE_WEBHOOK_RESPONSE, + // $this->client_subscription->client, + // ); } diff --git a/app/Transformers/SubscriptionTransformer.php b/app/Transformers/SubscriptionTransformer.php index 4c0b59d2118b..da8b892d320c 100644 --- a/app/Transformers/SubscriptionTransformer.php +++ b/app/Transformers/SubscriptionTransformer.php @@ -38,10 +38,12 @@ class SubscriptionTransformer extends EntityTransformer return [ 'id' => $this->encodePrimaryKey($subscription->id), 'user_id' => $this->encodePrimaryKey($subscription->user_id), - 'product_id' => $this->encodePrimaryKey($subscription->product_id), + 'group_id' => $this->encodePrimaryKey($subscription->group_id), + 'product_ids' => $subscription->product_ids, + 'recurring_product_ids' => $subscription->recurring_product_ids, 'assigned_user_id' => $this->encodePrimaryKey($subscription->assigned_user_id), 'company_id' => $this->encodePrimaryKey($subscription->company_id), - 'is_recurring' => (bool)$subscription->is_recurring, + 'price' => (float) $subscription->price, 'frequency_id' => (string)$subscription->frequency_id, 'auto_bill' => (string)$subscription->auto_bill, 'promo_code' => (string)$subscription->promo_code, diff --git a/app/Utils/HtmlEngine.php b/app/Utils/HtmlEngine.php index ca1442ff0281..004ef0d1dc1a 100644 --- a/app/Utils/HtmlEngine.php +++ b/app/Utils/HtmlEngine.php @@ -192,6 +192,7 @@ class HtmlEngine $data['$taxes'] = ['value' => Number::formatMoney($this->entity_calc->getItemTotalTaxes(), $this->client) ?: ' ', 'label' => ctrans('texts.taxes')]; $data['$invoice.taxes'] = &$data['$taxes']; + $data['$user_iban'] = ['value' => $this->helpers->formatCustomFieldValue($this->company->custom_fields, 'company1', $this->settings->custom_value1, $this->client) ?: ' ', 'label' => $this->helpers->makeCustomField($this->company->custom_fields, 'company1')]; $data['$invoice.custom1'] = ['value' => $this->helpers->formatCustomFieldValue($this->company->custom_fields, 'invoice1', $this->entity->custom_value1, $this->client) ?: ' ', 'label' => $this->helpers->makeCustomField($this->company->custom_fields, 'invoice1')]; $data['$invoice.custom2'] = ['value' => $this->helpers->formatCustomFieldValue($this->company->custom_fields, 'invoice2', $this->entity->custom_value2, $this->client) ?: ' ', 'label' => $this->helpers->makeCustomField($this->company->custom_fields, 'invoice2')]; $data['$invoice.custom3'] = ['value' => $this->helpers->formatCustomFieldValue($this->company->custom_fields, 'invoice3', $this->entity->custom_value3, $this->client) ?: ' ', 'label' => $this->helpers->makeCustomField($this->company->custom_fields, 'invoice3')]; @@ -287,7 +288,7 @@ class HtmlEngine $data['$signature'] = ['value' => $this->settings->email_signature ?: ' ', 'label' => '']; - $data['$spc_qr_code'] = ['value' => $this->company->present()->getSpcQrCode($this->client->currency()->code, $this->entity->number, $this->entity->balance), 'label' => '']; + $data['$spc_qr_code'] = ['value' => $this->company->present()->getSpcQrCode($this->client->currency()->code, $this->entity->number, $this->entity->balance, $this->helpers->formatCustomFieldValue($this->company->custom_fields, 'company1', $this->settings->custom_value1, $this->client)), 'label' => '']; $logo = $this->company->present()->logo($this->settings); diff --git a/database/migrations/2021_03_26_201148_add_price_column_to_subscriptions_table.php b/database/migrations/2021_03_26_201148_add_price_column_to_subscriptions_table.php new file mode 100644 index 000000000000..ffa8f5923208 --- /dev/null +++ b/database/migrations/2021_03_26_201148_add_price_column_to_subscriptions_table.php @@ -0,0 +1,33 @@ +decimal('price', 20, 6)->default(0); + $table->decimal('promo_price', 20, 6)->default(0); + }); + } + + /** + * Reverse the migrations. + * + * @return void + */ + public function down() + { + Schema::table('subscriptions', function (Blueprint $table) { + // + }); + } +} diff --git a/tests/Feature/SubscriptionApiTest.php b/tests/Feature/SubscriptionApiTest.php index f5b8e424f4fd..8c4ec16f2f00 100644 --- a/tests/Feature/SubscriptionApiTest.php +++ b/tests/Feature/SubscriptionApiTest.php @@ -80,6 +80,7 @@ class SubscriptionApiTest extends TestCase 'X-API-TOKEN' => $this->token, ])->post('/api/v1/subscriptions', ['product_ids' => $product->id, 'allow_cancellation' => true]); + // nlog($response); $response->assertStatus(200); }