diff --git a/app/Jobs/Util/SubscriptionHandler.php b/app/Jobs/Util/SubscriptionHandler.php index bbc28d6dbccb..fb944ce7da5e 100644 --- a/app/Jobs/Util/SubscriptionHandler.php +++ b/app/Jobs/Util/SubscriptionHandler.php @@ -43,7 +43,7 @@ class SubscriptionHandler implements ShouldQueue if(!$this->entity->company || $this->entity->company->company_users->first()->is_migrating) return true; - info("i got past the check"); + //info("i got past the check"); $subscriptions = Subscription::where('company_id', $this->entity->company_id) ->where('event_id', $this->event_id) diff --git a/app/Listeners/Invoice/CreateInvoicePdf.php b/app/Listeners/Invoice/CreateInvoicePdf.php index 6db605b83f41..b7ca4eeb33ea 100644 --- a/app/Listeners/Invoice/CreateInvoicePdf.php +++ b/app/Listeners/Invoice/CreateInvoicePdf.php @@ -35,6 +35,10 @@ class CreateInvoicePdf implements ShouldQueue */ public function handle($event) { - PdfCreator::dispatch($event->invoice->invitations->first()); + $event->invoice->invitations->each(function ($invitation) { + + PdfCreator::dispatch($invitation); + + }); } } diff --git a/app/Models/Payment.php b/app/Models/Payment.php index cac400cf7404..53612378747a 100644 --- a/app/Models/Payment.php +++ b/app/Models/Payment.php @@ -201,7 +201,9 @@ class Payment extends BaseModel public function refund(array $data) :Payment { - return $this->processRefund($data); + return $this->service()->refundPayment($data); + + //return $this->processRefund($data); } /** diff --git a/app/Repositories/BaseRepository.php b/app/Repositories/BaseRepository.php index d02986b7651f..6a1084bec687 100644 --- a/app/Repositories/BaseRepository.php +++ b/app/Repositories/BaseRepository.php @@ -11,6 +11,7 @@ namespace App\Repositories; +use App\Events\Invoice\InvoiceWasUpdated; use App\Factory\InvoiceInvitationFactory; use App\Factory\QuoteInvitationFactory; use App\Jobs\Product\UpdateOrCreateProduct; @@ -294,6 +295,9 @@ class BaseRepository } $model = $model->calc()->getInvoice(); + + event(new InvoiceWasUpdated($model, $model->company)); + } if ($class->name == Credit::class) { diff --git a/app/Services/Payment/PaymentService.php b/app/Services/Payment/PaymentService.php index 597038b5b298..615a9400d849 100644 --- a/app/Services/Payment/PaymentService.php +++ b/app/Services/Payment/PaymentService.php @@ -14,6 +14,7 @@ namespace App\Services\Payment; use App\Factory\PaymentFactory; use App\Models\Invoice; use App\Models\Payment; +use App\Services\Payment\RefundPayment; use App\Services\Payment\UpdateInvoicePayment; class PaymentService @@ -71,7 +72,12 @@ class PaymentService ->save(); } - public function updateInvoicePayment() + public function refundPayment(array $data) :?Payment + { + return ((new RefundPayment($this->payment, $data)))->run(); + } + + public function updateInvoicePayment() :?Payment { return ((new UpdateInvoicePayment($this->payment)))->run(); } diff --git a/app/Services/Payment/RefundPayment.php b/app/Services/Payment/RefundPayment.php new file mode 100644 index 000000000000..ba17898919c5 --- /dev/null +++ b/app/Services/Payment/RefundPayment.php @@ -0,0 +1,375 @@ +payment = $payment; + + $this->refund_data = $refund_data; + + $this->total_refund = 0; + + $this->gateway_refund_status = false; + } + + /** + */ + public function run() + { + + return $this->calculateTotalRefund() //sets amount for the refund (needed if we are refunding multiple invoices in one payment) + ->setStatus() //sets status of payment + ->buildCreditNote() //generate the credit note + ->buildCreditLineItems() //generate the credit note items + ->updateCreditables() //return the credits first + ->updatePaymentables() //update the paymentable items + ->createActivity() // create the refund activity + ->processGatewayRefund() //process the gateway refund if needed + ->save(); + } + + private function processGatewayRefund() + { + + if ($this->refund_data['gateway_refund'] !== false && $this->total_refund > 0) { + + $gateway = CompanyGateway::find($this->company_gateway_id); + + if ($gateway) { + $response = $gateway->driver($this->payment->client)->refund($this->payment, $this->total_refund); + + if (!$response) { + throw new PaymentRefundFailed(); + } + + //todo + //need to check the gateway response has successfully be transacted. + + //if a credit has been generated I think this is the correct section to fix the balance of the credit + } + } + else + $this->payment->refunded += $this->total_refund; + + + return $this; + } + + private function createActivity() + { + $fields = new \stdClass; + $activity_repo = new ActivityRepository(); + + $fields->payment_id = $this->payment->id; + $fields->user_id = $this->payment->user_id; + $fields->company_id = $this->payment->company_id; + $fields->activity_type_id = Activity::REFUNDED_PAYMENT; + $fields->credit_id = $this->credit_note->id; + + if (isset($this->refund_data['invoices'])) { + foreach ($this->refund_data['invoices'] as $invoice) { + $fields->invoice_id = $invoice['invoice_id']; + $activity_repo->save($fields, $this->payment); + } + } else { + $activity_repo->save($fields, $this->payment); + } + + return $this; + } + + private function calculateTotalRefund() + { + + if(isset($this->refund_data['invoices']) && count($this->refund_data['invoices']) > 0) + $this->total_refund = collect($this->refund_data['invoices'])->sum('amount'); + else + $this->total_refund = $this->refund_data['amount']; + + return $this; + } + + private function setStatus() + { + if ($this->refund_data['amount'] == $this->payment->amount) { + $this->payment->status_id = Payment::STATUS_REFUNDED; + } else { + $this->payment->status_id = Payment::STATUS_PARTIALLY_REFUNDED; + } + + return $this; + } + + private function buildCreditNote() + { + $this->credit_note = CreditFactory::create($this->payment->company_id, $this->payment->user_id); + $this->credit_note->assigned_user_id = isset($this->payment->assigned_user_id) ?: null; + $this->credit_note->date = $this->refund_data['date']; + $this->credit_note->status_id = Credit::STATUS_SENT; + $this->credit_note->client_id = $this->payment->client->id; + $this->credit_note->amount = $this->refund_data['amount']; + $this->credit_note->balance = $this->refund_data['amount']; + + $this->credit_note->save(); + $this->credit_note->number = $this->payment->client->getNextCreditNumber($this->payment->client); + $this->credit_note->save(); + + return $this; + } + + private function buildCreditLineItems() + { + $ledger_string = ''; + + if(isset($this->refund_data['invoices']) && count($this->refund_data['invoices']) > 0) + { + foreach ($this->refund_data['invoices'] as $invoice) + { + + $inv = Invoice::find($invoice['invoice_id']); + + $credit_line_item = InvoiceItemFactory::create(); + $credit_line_item->quantity = 1; + $credit_line_item->cost = $invoice['amount']; + $credit_line_item->product_key = ctrans('texts.invoice'); + $credit_line_item->notes = ctrans('texts.refund_body', ['amount' => $invoice['amount'], 'invoice_number' => $inv->number]); + $credit_line_item->line_total = $invoice['amount']; + $credit_line_item->date = $this->refund_data['date']; + + $ledger_string .= $credit_line_item->notes . ' '; + + $line_items[] = $credit_line_item; + } + } + else + { + + $credit_line_item = InvoiceItemFactory::create(); + $credit_line_item->quantity = 1; + $credit_line_item->cost = $this->refund_data['amount']; + $credit_line_item->product_key = ctrans('texts.credit'); + $credit_line_item->notes = ctrans('texts.credit_created_by', ['transaction_reference' => $this->payment->number]); + $credit_line_item->line_total = $this->refund_data['amount']; + $credit_line_item->date = $this->refund_data['date']; + + $line_items = []; + $line_items[] = $credit_line_item; + } + + $this->credit_note->line_items = $line_items; + $this->credit_note->save(); + + return $this; + } + + private function updatePaymentables() + { + if(isset($this->refund_data['invoices']) && count($this->refund_data['invoices']) > 0) + { + $this->payment->invoices->each(function ($paymentable_invoice) { + + collect($this->refund_data['invoices'])->each(function ($refunded_invoice) use($paymentable_invoice){ + + if($refunded_invoice['invoice_id'] == $paymentable_invoice->id) + { + $paymentable_invoice->pivot->refunded += $refunded_invoice['amount']; + $paymentable_invoice->pivot->save(); + } + + }); + + }); + } + + return $this; + } + + private function updateCreditables() + { + + if ($this->payment->credits()->exists()) { + //Adjust credits first!!! + foreach ($this->payment->credits as $paymentable_credit) { + $available_credit = $paymentable_credit->pivot->amount - $paymentable_credit->pivot->refunded; + + if ($available_credit > $this->total_refund) { + $paymentable_credit->pivot->refunded += $this->total_refund; + $paymentable_credit->pivot->save(); + + $paymentable_credit->balance += $this->total_refund; + $paymentable_credit->save(); + + $this->total_refund = 0; + } else { + $paymentable_credit->pivot->refunded += $available_credit; + $paymentable_credit->pivot->save(); + + $paymentable_credit->balance += $available_credit; + $paymentable_credit->save(); + + $this->total_refund -= $available_credit; + } + + if ($this->total_refund == 0) { + break; + } + } + } + + return $this; + } + + private function save() + { + $this->payment->save(); + + return $this->payment; + } +} + + + + + +/* + + private function refundPaymentWithNoInvoices(array $data) + { + + $this->createActivity($data, $credit_note->id); + + //determine if we need to refund via gateway + if ($data['gateway_refund'] !== false) { + //todo process gateway refund, on success, reduce the credit note balance to 0 + } + + $this->save(); + + //$this->client->paid_to_date -= $data['amount']; + $this->client->save(); + + return $this->fresh(); + } + + + private function refundPaymentWithInvoices($data) + { + + if ($data['gateway_refund'] !== false && $this->total_refund > 0) { + $gateway = CompanyGateway::find($this->company_gateway_id); + + if ($gateway) { + $response = $gateway->driver($this->client)->refund($this, $this->total_refund); + + if (!$response) { + throw new PaymentRefundFailed(); + } + } + } + + if ($this->total_refund > 0) { + $this->refunded += $this->total_refund; + } + + $this->save(); + + $client_balance_adjustment = $this->adjustInvoices($data); + + $credit_note->ledger()->updateCreditBalance($client_balance_adjustment, $ledger_string); + + $this->client->paid_to_date -= $data['amount']; + $this->client->save(); + + + return $this; + } + + private function createActivity(array $data, int $credit_id) + { + $fields = new \stdClass; + $activity_repo = new ActivityRepository(); + + $fields->payment_id = $this->id; + $fields->user_id = $this->user_id; + $fields->company_id = $this->company_id; + $fields->activity_type_id = Activity::REFUNDED_PAYMENT; + $fields->credit_id = $credit_id; + + if (isset($data['invoices'])) { + foreach ($data['invoices'] as $invoice) { + $fields->invoice_id = $invoice->id; + + $activity_repo->save($fields, $this); + } + } else { + $activity_repo->save($fields, $this); + } + } + + + private function buildCreditNote(array $data) :?Credit + { + $credit_note = CreditFactory::create($this->company_id, $this->user_id); + $credit_note->assigned_user_id = isset($this->assigned_user_id) ?: null; + $credit_note->date = $data['date']; + $credit_note->status_id = Credit::STATUS_SENT; + $credit_note->client_id = $this->client->id; + $credit_note->amount = $data['amount']; + $credit_note->balance = $data['amount']; + + return $credit_note; + } + + private function adjustInvoices(array $data) + { + $adjustment_amount = 0; + + foreach ($data['invoices'] as $refunded_invoice) { + $invoice = Invoice::find($refunded_invoice['invoice_id']); + + $invoice->service()->updateBalance($refunded_invoice['amount'])->save(); + + if ($invoice->amount == $invoice->balance) { + $invoice->service()->setStatus(Invoice::STATUS_SENT); + } else { + $invoice->service()->setStatus(Invoice::STATUS_PARTIAL); + } + + $client = $invoice->client; + + $adjustment_amount += $refunded_invoice['amount']; + $client->balance += $refunded_invoice['amount']; + + $client->save(); + + //todo adjust ledger balance here? or after and reference the credit and its total + } + + return $adjustment_amount; + } +} + + */ \ No newline at end of file diff --git a/routes/api.php b/routes/api.php index b88743d8309c..58b2e6dbec0f 100644 --- a/routes/api.php +++ b/routes/api.php @@ -112,7 +112,7 @@ Route::group(['middleware' => ['api_db', 'token_auth', 'locale'], 'prefix' => 'a Route::post('templates', 'TemplateController@show')->name('templates.show'); - Route::post('preview', 'PreviewController@show'); + Route::post('preview', 'PreviewController@show')->name('preview.show'); Route::post('self-update', 'SelfUpdateController@update')->middleware('password_protected'); diff --git a/tests/Feature/PreviewTest.php b/tests/Feature/PreviewTest.php deleted file mode 100644 index 2b608c0b134d..000000000000 --- a/tests/Feature/PreviewTest.php +++ /dev/null @@ -1,89 +0,0 @@ -makeTestData(); - - Session::start(); - - $this->faker = \Faker\Factory::create(); - - Model::reguard(); - - - if (config('ninja.testvars.travis') !== false) { - $this->markTestSkipped('Skip test for CI Testing'); - } - - } - - - public function testPreviewDesign() - { - $design = Design::find(3); - - $data = [ - 'entity' => 'invoice', - 'entity_id' => $this->invoice->hashed_id, - 'design' => $design, - - ]; - - $response = $this->withHeaders([ - 'X-API-SECRET' => config('ninja.api_secret'), - 'X-API-TOKEN' => $this->token - ])->post('/api/v1/preview', $data)->assertStatus(200); - } - - - public function testBlankEntityPreviewDesign() - { - $design = Design::find(3); - - $data = [ - 'design' => $design, - ]; - - - $response = $this->withHeaders([ - 'X-API-SECRET' => config('ninja.api_secret'), - 'X-API-TOKEN' => $this->token - ])->post('/api/v1/preview', $data); - - - $response->assertStatus(200); - } -} diff --git a/tests/Feature/RefundTest.php b/tests/Feature/RefundTest.php index 898ac90a82ab..1df44379e248 100644 --- a/tests/Feature/RefundTest.php +++ b/tests/Feature/RefundTest.php @@ -125,6 +125,8 @@ class RefundTest extends TestCase $response->assertStatus(200); +info($arr); + $this->assertEquals(50, $arr['data']['refunded']); $this->assertEquals(Payment::STATUS_REFUNDED, $arr['data']['status_id']); }