Merge remote-tracking branch 'upstream/v2' into v2-2805-client-signup

This commit is contained in:
Benjamin Beganović 2020-06-18 15:57:05 +02:00
commit 22aa182250
212 changed files with 140466 additions and 106590 deletions

3
.babelrc Normal file
View File

@ -0,0 +1,3 @@
{
"plugins": ["@babel/plugin-proposal-class-properties"]
}

View File

@ -43,6 +43,7 @@ MAIL_FROM_ADDRESS='user@example.com'
MAIL_FROM_NAME='Self Hosted User' MAIL_FROM_NAME='Self Hosted User'
POSTMARK_API_TOKEN= POSTMARK_API_TOKEN=
REQUIRE_HTTPS=true
GOOGLE_MAPS_API_KEY= GOOGLE_MAPS_API_KEY=
API_SECRET=superdoopersecrethere API_SECRET=superdoopersecrethere

1
.gitignore vendored
View File

@ -26,6 +26,5 @@ local_version.txt
# Ignore local migrations # Ignore local migrations
storage/migrations storage/migrations
nbproject nbproject
/composer.lock
.php_cs.cache .php_cs.cache

View File

@ -1,5 +1,6 @@
{ {
"semi": true, "semi": true,
"singleQuote": true, "singleQuote": true,
"trailingComma": "es5" "trailingComma": "es5",
"arrowParens": "always"
} }

View File

@ -26,7 +26,7 @@ npm i
npm run production npm run production
``` ```
Please Note: Your APP_KEY in the .env file is used to encrypt data, if you loose this you will not be able to run the application. Please Note: Your APP_KEY in the .env file is used to encrypt data, if you lose this you will not be able to run the application.
Run if you want to load sample data, remember to configure .env Run if you want to load sample data, remember to configure .env
``` ```

View File

@ -299,7 +299,7 @@ class CheckData extends Command
$ledger = CompanyLedger::where('client_id', $client->id)->orderBy('id', 'DESC')->first(); $ledger = CompanyLedger::where('client_id', $client->id)->orderBy('id', 'DESC')->first();
if($ledger && $invoice_balance != $client->balance) if($ledger && number_format($invoice_balance, 4) != number_format($client->balance, 4))
{ {
$wrong_balances++; $wrong_balances++;
$this->logMessage($client->present()->name . " - " . $client->id . " - balances do not match {$invoice_balance} - {$client->balance} - {$ledger->balance}"); $this->logMessage($client->present()->name . " - " . $client->id . " - balances do not match {$invoice_balance} - {$client->balance} - {$ledger->balance}");

View File

@ -25,6 +25,7 @@ use App\Models\PaymentType;
use App\Models\Product; use App\Models\Product;
use App\Models\User; use App\Models\User;
use App\Repositories\InvoiceRepository; use App\Repositories\InvoiceRepository;
use App\Utils\Traits\GeneratesCounter;
use App\Utils\Traits\MakesHash; use App\Utils\Traits\MakesHash;
use Carbon\Carbon; use Carbon\Carbon;
use Faker\Factory; use Faker\Factory;
@ -35,7 +36,7 @@ use Illuminate\Support\Str;
class CreateTestData extends Command class CreateTestData extends Command
{ {
use MakesHash; use MakesHash, GeneratesCounter;
/** /**
* @var string * @var string
*/ */
@ -233,7 +234,7 @@ class CreateTestData extends Command
$this->createClient($company, $user); $this->createClient($company, $user);
} }
for($x=0; $x<$this->count; $x++) for($x=0; $x<$this->count*100; $x++)
{ {
$client = $company->clients->random(); $client = $company->clients->random();
@ -314,7 +315,7 @@ class CreateTestData extends Command
]); ]);
factory(\App\Models\Product::class, 50)->create([ factory(\App\Models\Product::class, 15000)->create([
'user_id' => $user->id, 'user_id' => $user->id,
'company_id' => $company->id, 'company_id' => $company->id,
]); ]);
@ -324,7 +325,7 @@ class CreateTestData extends Command
$this->info('Creating '.$this->count. ' clients'); $this->info('Creating '.$this->count. ' clients');
for ($x=0; $x<$this->count; $x++) { for ($x=0; $x<$this->count*1000; $x++) {
$z = $x+1; $z = $x+1;
$this->info("Creating client # ".$z); $this->info("Creating client # ".$z);
@ -393,6 +394,10 @@ class CreateTestData extends Command
'client_id' => $client->id, 'client_id' => $client->id,
'company_id' => $company->id 'company_id' => $company->id
]); ]);
$client->id_number = $this->getNextClientNumber($client);
$client->save();
} }
private function createExpense($client) private function createExpense($client)

View File

@ -13,6 +13,7 @@ namespace App\Console;
use App\Jobs\Cron\RecurringInvoicesCron; use App\Jobs\Cron\RecurringInvoicesCron;
use App\Jobs\Ninja\AdjustEmailQuota; use App\Jobs\Ninja\AdjustEmailQuota;
use App\Jobs\Ninja\CheckDbStatus;
use App\Jobs\Util\ReminderJob; use App\Jobs\Util\ReminderJob;
use App\Jobs\Util\SendFailedEmails; use App\Jobs\Util\SendFailedEmails;
use App\Jobs\Util\UpdateExchangeRates; use App\Jobs\Util\UpdateExchangeRates;
@ -52,6 +53,7 @@ class Kernel extends ConsoleKernel
if(Ninja::isHosted()) { if(Ninja::isHosted()) {
$schedule->job(new AdjustEmailQuota())->daily(); $schedule->job(new AdjustEmailQuota())->daily();
$schedule->job(new SendFailedEmails())->daily(); $schedule->job(new SendFailedEmails())->daily();
$schedule->job(new CheckDbStatus())->everyFiveMinutes();
} }
/* Run queue's in shared hosting with this*/ /* Run queue's in shared hosting with this*/
if (Ninja::isSelfHost()) { if (Ninja::isSelfHost()) {

View File

@ -33,7 +33,7 @@ class CompanySettings extends BaseSettings
public $enable_client_portal_dashboard = true; //implemented public $enable_client_portal_dashboard = true; //implemented
public $signature_on_pdf = false; public $signature_on_pdf = false;
public $document_email_attachment = false; public $document_email_attachment = false;
public $send_portal_password = false; //public $send_portal_password = false;
public $portal_design_id = '1'; public $portal_design_id = '1';
@ -113,7 +113,7 @@ class CompanySettings extends BaseSettings
public $invoice_terms = ''; public $invoice_terms = '';
public $quote_terms = ''; public $quote_terms = '';
public $invoice_taxes = 0; public $invoice_taxes = 0;
public $enabled_item_tax_rates = 0; // public $enabled_item_tax_rates = 0;
public $invoice_design_id = 'VolejRejNm'; public $invoice_design_id = 'VolejRejNm';
public $quote_design_id = 'VolejRejNm'; public $quote_design_id = 'VolejRejNm';
public $credit_design_id = 'VolejRejNm'; public $credit_design_id = 'VolejRejNm';
@ -274,7 +274,7 @@ class CompanySettings extends BaseSettings
'email_template_statement' => 'string', 'email_template_statement' => 'string',
'email_subject_statement' => 'string', 'email_subject_statement' => 'string',
'signature_on_pdf' => 'bool', 'signature_on_pdf' => 'bool',
'send_portal_password' => 'bool', // 'send_portal_password' => 'bool',
'quote_footer' => 'string', 'quote_footer' => 'string',
'page_size' => 'string', 'page_size' => 'string',
'font_size' => 'int', 'font_size' => 'int',
@ -344,7 +344,7 @@ class CompanySettings extends BaseSettings
'invoice_design_id' => 'string', 'invoice_design_id' => 'string',
'invoice_fields' => 'string', 'invoice_fields' => 'string',
'invoice_taxes' => 'int', 'invoice_taxes' => 'int',
'enabled_item_tax_rates' => 'int', //'enabled_item_tax_rates' => 'int',
'invoice_footer' => 'string', 'invoice_footer' => 'string',
'invoice_labels' => 'string', 'invoice_labels' => 'string',
'invoice_terms' => 'string', 'invoice_terms' => 'string',

View File

@ -33,7 +33,7 @@ class FreeCompanySettings extends BaseSettings
public $custom_value3 = ''; public $custom_value3 = '';
public $custom_value4 = ''; public $custom_value4 = '';
public $date_format_id = ''; public $date_format_id = '';
public $enabled_item_tax_rates = 0; // public $enabled_item_tax_rates = 0;
public $expense_number_pattern = ''; public $expense_number_pattern = '';
public $expense_number_counter = 1; public $expense_number_counter = 1;
public $inclusive_taxes = false; public $inclusive_taxes = false;

View File

@ -63,7 +63,7 @@ class Bold extends AbstractDesign
</div> </div>
<div class="col-span-5"> <div class="col-span-5">
<div class="bg-teal-600 px-5 py-3 text-white"> <div class="bg-teal-600 px-5 py-3 text-white">
<div class="w-80 flex flex-col text-white"> <div class="w-80 flex flex-col text-white flex-wrap">
$entity_details $entity_details
</div> </div>
</div> </div>
@ -76,7 +76,7 @@ class Bold extends AbstractDesign
<thead class="text-left"> <thead class="text-left">
$product_table_header $product_table_header
</thead> </thead>
<tbody> <tbody class="whitespace-pre-line">
$product_table_body $product_table_body
</tbody> </tbody>
</table> </table>
@ -84,7 +84,7 @@ class Bold extends AbstractDesign
<thead class="text-left"> <thead class="text-left">
$task_table_header $task_table_header
</thead> </thead>
<tbody> <tbody class="whitespace-pre-line">
$task_table_body $task_table_body
</tbody> </tbody>
</table> </table>

View File

@ -52,10 +52,10 @@ $custom_css
<div class="col-span-2 p-3"> <div class="col-span-2 p-3">
$company_logo $company_logo
</div> </div>
<div class="col-span-2 p-3 flex flex-col"> <div class="col-span-2 p-3 flex flex-col flex-wrap">
$company_details $company_details
</div> </div>
<div class="col-span-2 p-3 flex flex-col"> <div class="col-span-2 p-3 flex flex-col flex-wrap">
$company_address $company_address
</div> </div>
</div>'; </div>';
@ -64,11 +64,11 @@ $custom_css
public function body() public function body()
{ {
return '<div class="grid grid-cols-12 gap-1 mt-8"> return '<div class="grid grid-cols-12 gap-1 mt-8">
<div class="col-span-7 p-3 flex flex-col"> <div class="col-span-7 p-3 flex flex-col flex-wrap">
$client_details $client_details
</div> </div>
<div class="col-span-5 p-3 flex flex-col bg-orange-600 px-4 py-4 h-auto rounded-lg"> <div class="col-span-5 p-3 flex flex-col bg-orange-600 px-4 py-4 h-auto rounded-lg">
<div class="flex flex-col text-white"> <div class="flex flex-col text-white flex-wrap">
$entity_details $entity_details
</div> </div>
</div> </div>
@ -78,7 +78,7 @@ $custom_css
<thead class="text-left"> <thead class="text-left">
$product_table_header $product_table_header
</thead> </thead>
<tbody class="bg-gray-200"> <tbody class="bg-gray-200 whitespace-pre-line">
$product_table_body $product_table_body
</tbody> </tbody>
</table> </table>
@ -86,7 +86,7 @@ $custom_css
<thead class="text-left"> <thead class="text-left">
$task_table_header $task_table_header
</thead> </thead>
<tbody class="bg-gray-200"> <tbody class="bg-gray-200 whitespace-pre-line">
$task_table_body $task_table_body
</tbody> </tbody>
</table> </table>

View File

@ -48,10 +48,10 @@ class Clean extends AbstractDesign
<div class="h-14 w-14">$company_logo</div> <div class="h-14 w-14">$company_logo</div>
</div> </div>
<div class="w-auto flex"> <div class="w-auto flex">
<div class="mr-10 text-gray-600 flex flex-col"> <div class="mr-10 text-gray-600 flex flex-col flex-wrap">
$company_details $company_details
</div> </div>
<div class="ml-5 text-gray-600 flex flex-col"> <div class="ml-5 text-gray-600 flex flex-col flex-wrap">
$company_address $company_address
</div> </div>
</div> </div>
@ -68,13 +68,13 @@ class Clean extends AbstractDesign
<div class="ml-4 py-4"> <div class="ml-4 py-4">
<div class="flex"> <div class="flex">
<div class="w-40 flex flex-col"> <div class="w-40 flex flex-col flex-wrap">
$entity_labels $entity_labels
</div> </div>
<div class="w-48 flex flex-col"> <div class="w-48 flex flex-col flex-wrap">
$entity_details $entity_details
</div> </div>
<div class="w-56 flex flex-col"> <div class="w-56 flex flex-col flex-wrap">
$client_details $client_details
</div> </div>
</div> </div>
@ -84,7 +84,7 @@ class Clean extends AbstractDesign
<thead class="text-left"> <thead class="text-left">
$product_table_header $product_table_header
</thead> </thead>
<tbody> <tbody class="whitespace-pre-line">
$product_table_body $product_table_body
</tbody> </tbody>
</table> </table>
@ -92,7 +92,7 @@ class Clean extends AbstractDesign
<thead class="text-left"> <thead class="text-left">
$task_table_header $task_table_header
</thead> </thead>
<tbody> <tbody class="whitespace-pre-line">
$task_table_body $task_table_body
</tbody> </tbody>
</table> </table>

View File

@ -60,14 +60,14 @@ $custom_css
<div class="col-span-7"> <div class="col-span-7">
<p class="text-4xl text-pink-700">#$entity_number</p> <p class="text-4xl text-pink-700">#$entity_number</p>
</div> </div>
<div class="col-span-5 flex flex-col">$entity_details</div> <div class="col-span-5 flex flex-col flex-wrap">$entity_details</div>
</div> </div>
<table class="w-full table-auto border-t-4 border-pink-700 bg-white mt-8"> <table class="w-full table-auto border-t-4 border-pink-700 bg-white mt-8">
<thead class="text-left rounded-lg"> <thead class="text-left rounded-lg">
$product_table_header $product_table_header
</thead> </thead>
<tbody> <tbody class="whitespace-pre-line">
$product_table_body $product_table_body
</tbody> </tbody>
</table> </table>
@ -75,7 +75,7 @@ $custom_css
<thead class="text-left rounded-lg"> <thead class="text-left rounded-lg">
$task_table_header $task_table_header
</thead> </thead>
<tbody> <tbody class="whitespace-pre-line">
$task_table_body $task_table_body
</tbody> </tbody>
</table> </table>

View File

@ -107,7 +107,7 @@ class Designer
</div>' </div>'
; ;
$signature = '<div></div>'; /** @wip */ $signature = '<img class="h-40" src="$contact.signature" />';
$logo = '<div></div>'; $logo = '<div></div>';
if (!$this->entity->user->account->isPaid()) { if (!$this->entity->user->account->isPaid()) {

View File

@ -47,7 +47,7 @@ class Elegant extends AbstractDesign
<div class="col-span-8"> <div class="col-span-8">
$company_logo $company_logo
</div> </div>
<div class="col-span-4 flex flex-col"> <div class="col-span-4 flex flex-col flex-wrap">
$entity_details $entity_details
</div> </div>
</div> </div>
@ -57,13 +57,13 @@ class Elegant extends AbstractDesign
public function body() public function body()
{ {
return '<div class="grid grid-cols-12 gap-4 mt-8"> return '<div class="grid grid-cols-12 gap-4 mt-8">
<div class="col-span-4 mr-6 flex flex-col pr-2 border-r border-dashed border-black"> <div class="col-span-4 mr-6 flex flex-col pr-2 border-r border-dashed border-black flex-wrap">
$client_details $client_details
</div> </div>
<div class="col-span-4 flex flex-col mr-6"> <div class="col-span-4 flex flex-col mr-6 flex-wrap">
$company_details $company_details
</div> </div>
<div class="col-span-4 flex flex-col"> <div class="col-span-4 flex flex-col flex-wrap">
$company_address $company_address
</div> </div>
</div> </div>
@ -71,7 +71,7 @@ class Elegant extends AbstractDesign
<thead class="text-left border-dashed border-b border-black"> <thead class="text-left border-dashed border-b border-black">
$product_table_header $product_table_header
</thead> </thead>
<tbody> <tbody class="whitespace-pre-line">
$product_table_body $product_table_body
</tbody> </tbody>
</table> </table>
@ -79,7 +79,7 @@ class Elegant extends AbstractDesign
<thead class="text-left border-dashed border-b border-black"> <thead class="text-left border-dashed border-b border-black">
$task_table_header $task_table_header
</thead> </thead>
<tbody> <tbody class="whitespace-pre-line">
$task_table_body $task_table_body
</tbody> </tbody>
</table> </table>

View File

@ -48,15 +48,15 @@ $custom_css
<div class="w-1/2 border-l pl-4 border-black mr-4"> <div class="w-1/2 border-l pl-4 border-black mr-4">
<p class="font-semibold uppercase text-yellow-600">From:</p> <p class="font-semibold uppercase text-yellow-600">From:</p>
<div class="flex"> <div class="flex">
<div class="flex flex-col mr-5"> <div class="flex flex-col mr-5 flex-wrap">
$company_details $company_details
</div> </div>
<div class="flex flex-col"> <div class="flex flex-col flex-wrap">
$company_address $company_address
</div> </div>
</div> </div>
</div> </div>
<div class="w-1/3 border-l pl-4 border-black flex flex-col"> <div class="w-1/3 border-l pl-4 border-black flex flex-col flex-wrap">
<p class="font-semibold uppercase text-yellow-600">To:</p> <p class="font-semibold uppercase text-yellow-600">To:</p>
$client_details $client_details
</div> </div>
@ -90,7 +90,7 @@ $custom_css
<thead class="text-left"> <thead class="text-left">
$product_table_header $product_table_header
</thead> </thead>
<tbody> <tbody class="whitespace-pre-line">
$product_table_body $product_table_body
</tbody> </tbody>
</table> </table>
@ -98,7 +98,7 @@ $custom_css
<thead class="text-left"> <thead class="text-left">
$task_table_header $task_table_header
</thead> </thead>
<tbody> <tbody class="whitespace-pre-line">
$task_table_body $task_table_body
</tbody> </tbody>
</table> </table>

View File

@ -45,10 +45,10 @@ $custom_css
<div class="col-span-2 p-3"> <div class="col-span-2 p-3">
<h1 class="text-white font-bold text-3xl">$company.name</h1> <h1 class="text-white font-bold text-3xl">$company.name</h1>
</div> </div>
<div class="col-span-2 p-3 flex flex-col text-white"> <div class="col-span-2 p-3 flex flex-col text-white flex-wrap">
$company_details $company_details
</div> </div>
<div class="col-span-2 p-3 flex flex-col text-white"> <div class="col-span-2 p-3 flex flex-col text-white flex-wrap">
$entity_details $entity_details
</div> </div>
</div> </div>
@ -64,7 +64,7 @@ $custom_css
<div class="col-span-2 p-3"> <div class="col-span-2 p-3">
$company_logo $company_logo
</div> </div>
<div class="col-span-3 p-3 flex flex-col"> <div class="col-span-3 p-3 flex flex-col flex-wrap">
$client_details $client_details
</div> </div>
</div> </div>
@ -74,7 +74,7 @@ $custom_css
<thead class="text-left text-white bg-gray-900 display: table-header-group;"> <thead class="text-left text-white bg-gray-900 display: table-header-group;">
$product_table_header $product_table_header
</thead> </thead>
<tbody> <tbody class="whitespace-pre-line">
$product_table_body $product_table_body
</tbody> </tbody>
</table> </table>
@ -82,7 +82,7 @@ $custom_css
<thead class="text-left text-white bg-gray-900 display: table-header-group;"> <thead class="text-left text-white bg-gray-900 display: table-header-group;">
$task_table_header $task_table_header
</thead> </thead>
<tbody> <tbody class="whitespace-pre-line">
$task_table_body $task_table_body
</tbody> </tbody>
</table> </table>
@ -141,10 +141,10 @@ $custom_css
return ' return '
<div class="footer bg-orange-600 flex justify-between py-8 px-12" style="page-break-inside: avoid;"> <div class="footer bg-orange-600 flex justify-between py-8 px-12" style="page-break-inside: avoid;">
<div class="grid grid-cols-12 gap-4"> <div class="grid grid-cols-12 gap-4">
<div class="col-start-4 col-span-4 p-3 flex flex-col text-white text-right"> <div class="col-start-4 col-span-4 p-3 flex flex-col text-white text-right flex-wrap">
$company_details $company_details
</div> </div>
<div class="col-span-4 p-3 flex flex-col text-white text-right"> <div class="col-span-4 p-3 flex flex-col text-white text-right flex-wrap">
$company_address $company_address
</div> </div>
</div> </div>

View File

@ -53,7 +53,7 @@ $custom_css
$company_logo $company_logo
</div> </div>
<div class="col-span-5"> <div class="col-span-5">
<div class="flex flex-col"> <div class="flex flex-col flex-wrap">
$entity_details $entity_details
</div> </div>
</div> </div>
@ -67,13 +67,13 @@ $custom_css
<div class="flex flex-col"> <div class="flex flex-col">
<div class="flex"> <div class="flex">
<p class="uppercase text-orange-800">$to_label:</p> <p class="uppercase text-orange-800">$to_label:</p>
<div class="flex flex-col ml-2"> <div class="flex flex-col ml-2 flex-wrap">
$client_details $client_details
</div> </div>
</div> </div>
<div class="flex mt-5"> <div class="flex mt-5">
<p class="uppercase text-orange-800">$from_label:</p> <p class="uppercase text-orange-800">$from_label:</p>
<div class="flex flex-col ml-2"> <div class="flex flex-col ml-2 flex-wrap">
$company_details $company_details
</div> </div>
</div> </div>
@ -84,7 +84,7 @@ $custom_css
<thead class="text-left border-b-4 border-black"> <thead class="text-left border-b-4 border-black">
$product_table_header $product_table_header
</thead> </thead>
<tbody> <tbody class="whitespace-pre-line">
$product_table_body $product_table_body
</tbody> </tbody>
</table> </table>
@ -92,7 +92,7 @@ $custom_css
<thead class="text-left border-b-4 border-black"> <thead class="text-left border-b-4 border-black">
$task_table_header $task_table_header
</thead> </thead>
<tbody> <tbody class="whitespace-pre-line">
$task_table_body $task_table_body
</tbody> </tbody>
</table> </table>

View File

@ -45,10 +45,10 @@ $custom_css
<div class="col-span-2 p-3"> <div class="col-span-2 p-3">
$company_logo $company_logo
</div> </div>
<div class="col-span-2 p-3 flex flex-col"> <div class="col-span-2 p-3 flex flex-col flex-wrap">
$company_details $company_details
</div> </div>
<div class="col-span-2 p-3 flex flex-col"> <div class="col-span-2 p-3 flex flex-col flex-wrap">
$entity_details $entity_details
</div> </div>
</div>'; </div>';
@ -56,7 +56,7 @@ $custom_css
public function body() public function body()
{ {
return '<div class="flex flex-col mt-8"> return '<div class="flex flex-col mt-8 flex-wrap">
$client_details $client_details
</div> </div>
<table class="w-full table-auto mt-8"> <table class="w-full table-auto mt-8">

View File

@ -48,7 +48,7 @@ $custom_css
$company_logo $company_logo
</div> </div>
<div class="col-span-5 bg-teal-600 p-5 text-white"> <div class="col-span-5 bg-teal-600 p-5 text-white">
<div class="flex flex-col"> <div class="flex flex-col flex-wrap">
$entity_details $entity_details
</div> </div>
</div> </div>
@ -62,7 +62,7 @@ $custom_css
<div class="flex flex-col"> <div class="flex flex-col">
<p class="font-semibold text-teal-600 pl-4">$to_label:</p> <p class="font-semibold text-teal-600 pl-4">$to_label:</p>
<div class="flex border-dashed border-t-4 border-b-4 border-teal-600 py-4 mt-4 pl-4"> <div class="flex border-dashed border-t-4 border-b-4 border-teal-600 py-4 mt-4 pl-4">
<section class="flex flex-col"> <section class="flex flex-col flex-wrap">
$client_details $client_details
</section> </section>
</div> </div>
@ -72,7 +72,7 @@ $custom_css
<div class="flex flex-col"> <div class="flex flex-col">
<p class="font-semibold text-teal-600 pl-4">$from_label:</p> <p class="font-semibold text-teal-600 pl-4">$from_label:</p>
<div class="flex border-dashed border-t-4 border-b-4 border-teal-600 py-4 mt-4 pl-4"> <div class="flex border-dashed border-t-4 border-b-4 border-teal-600 py-4 mt-4 pl-4">
<section class="flex flex-col"> <section class="flex flex-col flex-wrap">
$company_details $company_details
</section> </section>
</div> </div>
@ -83,7 +83,7 @@ $custom_css
<thead class="text-left bg-teal-600 rounded-lg"> <thead class="text-left bg-teal-600 rounded-lg">
$product_table_header $product_table_header
</thead> </thead>
<tbody> <tbody class="whitespace-pre-line">
$product_table_body $product_table_body
</tbody> </tbody>
</table> </table>
@ -91,7 +91,7 @@ $custom_css
<thead class="text-left bg-teal-600 rounded-lg"> <thead class="text-left bg-teal-600 rounded-lg">
$task_table_header $task_table_header
</thead> </thead>
<tbody> <tbody class="whitespace-pre-line">
$task_table_body $task_table_body
</tbody> </tbody>
</table> </table>

View File

@ -0,0 +1,11 @@
<?php
namespace App\Exceptions;
use Exception;
use Throwable;
class GenericPaymentDriverFailure extends Exception
{
// ..
}

View File

@ -11,6 +11,7 @@
namespace App\Exceptions; namespace App\Exceptions;
use App\Exceptions\GenericPaymentDriverFailure;
use Exception; use Exception;
use Illuminate\Auth\Access\AuthorizationException; use Illuminate\Auth\Access\AuthorizationException;
use Illuminate\Auth\AuthenticationException; use Illuminate\Auth\AuthenticationException;
@ -115,8 +116,15 @@ class Handler extends ExceptionHandler
return response()->json(['message' => 'The given data was invalid.', 'errors' => $exception->validator->getMessageBag()], 422); return response()->json(['message' => 'The given data was invalid.', 'errors' => $exception->validator->getMessageBag()], 422);
} elseif ($exception instanceof RelationNotFoundException && $request->expectsJson()) { } elseif ($exception instanceof RelationNotFoundException && $request->expectsJson()) {
return response()->json(['message' => $exception->getMessage()], 400); return response()->json(['message' => $exception->getMessage()], 400);
} elseif ($exception instanceof GenericPaymentDriverFailure && $request->expectsJson()) {
return response()->json(['message' => $exception->getMessage()], 400);
} elseif ($exception instanceof GenericPaymentDriverFailure) {
$data['message'] = $exception->getMessage();
dd($data);
// return view('errors.layout', $data);
} }
return parent::render($request, $exception); return parent::render($request, $exception);
} }

View File

@ -91,7 +91,7 @@ class InvoiceItemSum
} }
private function sumLineItem() private function sumLineItem()
{ { //todo need to support quantities less than the precision amount
$this->setLineTotal($this->formatValue($this->item->cost, $this->currency->precision) * $this->formatValue($this->item->quantity, $this->currency->precision)); $this->setLineTotal($this->formatValue($this->item->cost, $this->currency->precision) * $this->formatValue($this->item->quantity, $this->currency->precision));
return $this; return $this;
} }

View File

@ -265,8 +265,8 @@ class BaseController extends Controller
'company.payments.paymentables', 'company.payments.paymentables',
'company.quotes.invitations.contact', 'company.quotes.invitations.contact',
'company.quotes.invitations.company', 'company.quotes.invitations.company',
'company.credits', 'company.credits.invitations.company',
'company.payment_terms', 'company.payment_terms.company',
//'company.credits.invitations.contact', //'company.credits.invitations.contact',
//'company.credits.invitations.company', //'company.credits.invitations.company',
'company.vendors.contacts', 'company.vendors.contacts',
@ -291,7 +291,7 @@ class BaseController extends Controller
* Thresholds for displaying large account on first load * Thresholds for displaying large account on first load
*/ */
if (request()->has('first_load') && request()->input('first_load') == 'true') { if (request()->has('first_load') && request()->input('first_load') == 'true') {
if (auth()->user()->getCompany()->invoices->count() > 1000) { if (auth()->user()->getCompany()->invoices->count() > 1000 || auth()->user()->getCompany()->products->count() > 1000 || auth()->user()->getCompany()->clients->count() > 1000) {
$data = $mini_load; $data = $mini_load;
} else { } else {
$data = $first_load; $data = $first_load;
@ -314,11 +314,10 @@ class BaseController extends Controller
public function flutterRoute() public function flutterRoute()
{ {
// // Ensure all request are over HTTPS in production if (config('ninja.require_https') && !request()->isSecure()) {
// if (! request()->secure()) { return redirect()->secure(request()->getRequestUri());
// return redirect()->secure(request()->path()); }
// }
if ((bool)$this->checkAppSetup() !== false && Schema::hasTable('accounts') && $account = Account::all()->first()) { if ((bool)$this->checkAppSetup() !== false && Schema::hasTable('accounts') && $account = Account::all()->first()) {
$data = []; $data = [];

View File

@ -52,6 +52,10 @@ class InvoiceController extends Controller
'invoice' => $invoice, 'invoice' => $invoice,
]; ];
if ($request->query('mode') === 'fullscreen') {
return $this->render('invoices.show.fullscreen', $data);
}
return $this->render('invoices.show', $data); return $this->render('invoices.show', $data);
} }

View File

@ -14,6 +14,7 @@ namespace App\Http\Controllers\ClientPortal;
use App\Filters\PaymentFilters; use App\Filters\PaymentFilters;
use App\Http\Controllers\Controller; use App\Http\Controllers\Controller;
use App\Jobs\Invoice\InjectSignature;
use App\Models\CompanyGateway; use App\Models\CompanyGateway;
use App\Models\Invoice; use App\Models\Invoice;
use App\Models\Payment; use App\Models\Payment;
@ -90,12 +91,18 @@ class PaymentController extends Controller
$invoices->map(function ($invoice) { $invoices->map(function ($invoice) {
$invoice->balance = Number::formatMoney($invoice->balance, $invoice->client); $invoice->balance = Number::formatMoney($invoice->balance, $invoice->client);
$invoice->due_date = $this->formatDate($invoice->due_date, $invoice->client->date_format()); $invoice->due_date = $this->formatDate($invoice->due_date, $invoice->client->date_format());
return $invoice; return $invoice;
}); });
if ((bool) request()->signature) {
$invoices->each(function ($invoice) {
InjectSignature::dispatch($invoice, request()->signature);
});
}
$payment_methods = auth()->user()->client->getPaymentMethods($amount); $payment_methods = auth()->user()->client->getPaymentMethods($amount);
$gateway = CompanyGateway::find(request()->input('company_gateway_id')); $gateway = CompanyGateway::find(request()->input('company_gateway_id'));
$payment_method_id = request()->input('payment_method_id'); $payment_method_id = request()->input('payment_method_id');
// Place to calculate gateway fee. // Place to calculate gateway fee.
@ -110,14 +117,19 @@ class PaymentController extends Controller
'hashed_ids' => request()->invoices, 'hashed_ids' => request()->invoices,
]; ];
return $gateway
return $gateway->driver(auth()->user()->client)->processPaymentView($data); ->driver(auth()->user()->client)
->setPaymentMethod($payment_method_id)
->processPaymentView($data);
} }
public function response(Request $request) public function response(Request $request)
{ {
$gateway = CompanyGateway::find($request->input('company_gateway_id')); $gateway = CompanyGateway::find($request->input('company_gateway_id'));
return $gateway->driver(auth()->user()->client)->processPaymentResponse($request); return $gateway
->driver(auth()->user()->client)
->setPaymentMethod($request->input('payment_method_id'))
->processPaymentResponse($request);
} }
} }

View File

@ -15,6 +15,8 @@ use App\Events\Payment\Methods\MethodDeleted;
use App\Http\Controllers\Controller; use App\Http\Controllers\Controller;
use App\Http\Requests\ClientPortal\CreatePaymentMethodRequest; use App\Http\Requests\ClientPortal\CreatePaymentMethodRequest;
use App\Models\ClientGatewayToken; use App\Models\ClientGatewayToken;
use App\Models\GatewayType;
use App\PaymentDrivers\AuthorizePaymentDriver;
use App\Utils\Traits\MakesDates; use App\Utils\Traits\MakesDates;
use Illuminate\Http\Request; use Illuminate\Http\Request;
use Illuminate\Support\Facades\Log; use Illuminate\Support\Facades\Log;
@ -42,13 +44,8 @@ class PaymentMethodController extends Controller
{ {
$gateway = auth()->user()->client->getCreditCardGateway(); $gateway = auth()->user()->client->getCreditCardGateway();
$data = [ return $gateway->driver(auth()->user()->client)->authorizeView(GatewayType::CREDIT_CARD);
'gateway' => $gateway,
'gateway_type_id' => 1,
'token' => false,
];
return $gateway->driver(auth()->user()->client)->authorizeCreditCardView($data);
} }
/** /**
@ -60,8 +57,9 @@ class PaymentMethodController extends Controller
public function store(Request $request) public function store(Request $request)
{ {
$gateway = auth()->user()->client->getCreditCardGateway(); $gateway = auth()->user()->client->getCreditCardGateway();
return $gateway->driver(auth()->user()->client)->authorizeResponseView($request->all());
return $gateway->driver(auth()->user()->client)->authorizeCreditCardResponse($request);
} }
/** /**
@ -100,6 +98,26 @@ class PaymentMethodController extends Controller
// //
} }
public function verify(ClientGatewayToken $payment_method)
{
$gateway = auth()->user()->client->getCreditCardGateway();
return $gateway
->driver(auth()->user()->client)
->setPaymentMethod('App\\PaymentDrivers\\Stripe\\ACH')
->verificationView($payment_method);
}
public function processVerification(ClientGatewaytoken $payment_method)
{
$gateway = auth()->user()->client->getCreditCardGateway();
return $gateway
->driver(auth()->user()->client)
->setPaymentMethod('App\\PaymentDrivers\\Stripe\\ACH')
->processVerification($payment_method);
}
/** /**
* Remove the specified resource from storage. * Remove the specified resource from storage.
* *

View File

@ -37,9 +37,15 @@ class QuoteController extends Controller
*/ */
public function show(ShowQuoteRequest $request, Quote $quote) public function show(ShowQuoteRequest $request, Quote $quote)
{ {
return $this->render('quotes.show', [ $data = [
'quote' => $quote, 'quote' => $quote,
]); ];
if ($request->query('mode') === 'fullscreen') {
return $this->render('quotes.show.fullscreen', $data);
}
return $this->render('quotes.show', $data);
} }
public function bulk(ProcessQuotesInBulkRequest $request) public function bulk(ProcessQuotesInBulkRequest $request)

View File

@ -11,6 +11,7 @@
namespace App\Http\Controllers; namespace App\Http\Controllers;
use App\DataMapper\CompanySettings;
use App\DataMapper\DefaultSettings; use App\DataMapper\DefaultSettings;
use App\Http\Requests\Company\CreateCompanyRequest; use App\Http\Requests\Company\CreateCompanyRequest;
use App\Http\Requests\Company\DestroyCompanyRequest; use App\Http\Requests\Company\DestroyCompanyRequest;
@ -218,6 +219,7 @@ class CompanyController extends BaseController
'is_locked' => 0, 'is_locked' => 0,
'permissions' => '', 'permissions' => '',
'settings' => null, 'settings' => null,
'notifications' => CompanySettings::notificationDefaults(),
//'settings' => DefaultSettings::userSettings(), //'settings' => DefaultSettings::userSettings(),
]); ]);
@ -464,11 +466,12 @@ class CompanyController extends BaseController
*/ */
public function destroy(DestroyCompanyRequest $request, Company $company) public function destroy(DestroyCompanyRequest $request, Company $company)
{ {
$company_count = $company->account->companies->count(); $company_count = $company->account->companies->count();
$account = $company->account; $account = $company->account;
if ($company_count == 1) { if ($company_count == 1) {
$company->company_users->each(function ($company_user) { $company->company_users->each(function ($company_user) {
$company_user->user->forceDelete(); $company_user->user->forceDelete();
}); });
@ -480,11 +483,13 @@ class CompanyController extends BaseController
} else { } else {
$company_id = $company->id; $company_id = $company->id;
$company->delete(); $company->delete();
//If we are deleting the default companies, we'll need to make a new company the default. //If we are deleting the default companies, we'll need to make a new company the default.
if ($account->default_company_id == $company_id) { if ($account->default_company_id == $company_id) {
$new_default_company = Company::whereAccountId($account->id)->first(); $new_default_company = Company::whereAccountId($account->id)->first();
$account->default_company_id = $new_default_company->id; $account->default_company_id = $new_default_company->id;
$account->save(); $account->save();

View File

@ -424,4 +424,73 @@ class CompanyGatewayController extends BaseController
return response()->json([], 200); return response()->json([], 200);
} }
/**
* Perform bulk actions on the list view
*
* @param BulkCompanyGatewayRequest $request
* @return \Illuminate\Http\Response
*
*
* @OA\Post(
* path="/api/v1/company_gateways/bulk",
* operationId="bulkCompanyGateways",
* tags={"company_gateways"},
* summary="Performs bulk actions on an array of company_gateways",
* description="",
* @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/index"),
* @OA\RequestBody(
* description="Array of company gateway IDs",
* required=true,
* @OA\MediaType(
* mediaType="application/json",
* @OA\Schema(
* type="array",
* @OA\Items(
* type="integer",
* description="Array of hashed IDs to be bulk 'actioned",
* example="[0,1,2,3]",
* ),
* )
* )
* ),
* @OA\Response(
* response=200,
* description="The Company Gateways response",
* @OA\Header(header="X-API-Version", ref="#/components/headers/X-API-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/CompanyGateway"),
* ),
* @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 bulk()
{
$action = request()->input('action');
$ids = request()->input('ids');
$company_gateways = CompanyGateway::withTrashed()->find($this->transformKeys($ids));
$company_gateways->each(function ($company_gateway, $key) use ($action) {
if (auth()->user()->can('edit', $company_gateway)) {
$this->company_repo->{$action}($company_gateway);
}
});
return $this->listResponse(CompanyGateway::withTrashed()->whereIn('id', $this->transformKeys($ids)));
}
} }

View File

@ -662,6 +662,13 @@ class InvoiceController extends BaseController
case 'download': case 'download':
return response()->download(TempFile::path($invoice->pdf_file_path()), basename($invoice->pdf_file_path())); return response()->download(TempFile::path($invoice->pdf_file_path()), basename($invoice->pdf_file_path()));
break; break;
case 'restore':
$this->invoice_repo->restore($invoice);
if (!$bulk) {
return $this->listResponse($invoice);
}
break;
case 'archive': case 'archive':
$this->invoice_repo->archive($invoice); $this->invoice_repo->archive($invoice);

View File

@ -66,7 +66,6 @@
* @OA\Property(property="invoice_terms", type="string", example="Invoice Terms are...", description="The default invoice terms"), * @OA\Property(property="invoice_terms", type="string", example="Invoice Terms are...", description="The default invoice terms"),
* @OA\Property(property="quote_terms", type="string", example="Quote Terms are...", description="The default quote terms"), * @OA\Property(property="quote_terms", type="string", example="Quote Terms are...", description="The default quote terms"),
* @OA\Property(property="invoice_taxes", type="number", example="1", description="Taxes can be applied to the invoice"), * @OA\Property(property="invoice_taxes", type="number", example="1", description="Taxes can be applied to the invoice"),
* @OA\Property(property="enabled_item_tax_rates", type="number", example="1", description="Taxes can be applied to the invoice items"),
* @OA\Property(property="invoice_design_id", type="string", example="1", description="The default design id (invoice, quote etc)"), * @OA\Property(property="invoice_design_id", type="string", example="1", description="The default design id (invoice, quote etc)"),
* @OA\Property(property="quote_design_id", type="string", example="1", description="The default design id (invoice, quote etc)"), * @OA\Property(property="quote_design_id", type="string", example="1", description="The default design id (invoice, quote etc)"),
* @OA\Property(property="invoice_footer", type="string", example="1", description="The default invoice footer"), * @OA\Property(property="invoice_footer", type="string", example="1", description="The default invoice footer"),
@ -130,7 +129,6 @@
* @OA\Property(property="email_template_statement", type="string", example="template matter", description="____________"), * @OA\Property(property="email_template_statement", type="string", example="template matter", description="____________"),
* @OA\Property(property="email_subject_statement", type="string", example="subject matter", description="____________"), * @OA\Property(property="email_subject_statement", type="string", example="subject matter", description="____________"),
* @OA\Property(property="signature_on_pdf", type="boolean", example=false, description="____________"), * @OA\Property(property="signature_on_pdf", type="boolean", example=false, description="____________"),
* @OA\Property(property="send_portal_password", type="boolean", example=false, description="____________"),
* @OA\Property(property="quote_footer", type="string", example="the quote footer", description="____________"), * @OA\Property(property="quote_footer", type="string", example="the quote footer", description="____________"),
* @OA\Property(property="email_subject_custom1", type="string", example="Custom Subject 1", description="____________"), * @OA\Property(property="email_subject_custom1", type="string", example="Custom Subject 1", description="____________"),
* @OA\Property(property="email_subject_custom2", type="string", example="Custom Subject 2", description="____________"), * @OA\Property(property="email_subject_custom2", type="string", example="Custom Subject 2", description="____________"),

View File

@ -12,7 +12,6 @@
namespace App\Http\Controllers; namespace App\Http\Controllers;
use App\Utils\Ninja; use App\Utils\Ninja;
use Codedge\Updater\UpdaterManager;
use Composer\Factory; use Composer\Factory;
use Composer\IO\NullIO; use Composer\IO\NullIO;
use Composer\Installer; use Composer\Installer;
@ -61,8 +60,10 @@ class SelfUpdateController extends BaseController
* ) * )
* *
*/ */
public function update(UpdaterManager $updater) public function update()
{ {
define('STDIN',fopen("php://stdin","r"));
if (Ninja::isNinja()) { if (Ninja::isNinja()) {
return response()->json(['message' => 'Self update not available on this system.'], 403); return response()->json(['message' => 'Self update not available on this system.'], 403);
} }

View File

@ -29,7 +29,8 @@ class Kernel extends HttpKernel
\App\Http\Middleware\TrimStrings::class, \App\Http\Middleware\TrimStrings::class,
\Illuminate\Foundation\Http\Middleware\ConvertEmptyStringsToNull::class, \Illuminate\Foundation\Http\Middleware\ConvertEmptyStringsToNull::class,
\App\Http\Middleware\TrustProxies::class, \App\Http\Middleware\TrustProxies::class,
\App\Http\Middleware\Cors::class, \App\Http\Middleware\Cors::class,
]; ];
/** /**
@ -54,6 +55,7 @@ class Kernel extends HttpKernel
'bindings', 'bindings',
'query_logging', 'query_logging',
\App\Http\Middleware\StartupCheck::class, \App\Http\Middleware\StartupCheck::class,
# \App\Http\Middleware\Cors::class,
], ],
'contact' => [ 'contact' => [
'throttle:60,1', 'throttle:60,1',

View File

@ -17,7 +17,7 @@ class CreditsTable extends Component
public function render() public function render()
{ {
$query = Credit::query() $query = Credit::query()
->where('company_id', auth('contact')->user()->company->id) ->where('client_id', auth('contact')->user()->client->id)
->orderBy($this->sort_field, $this->sort_asc ? 'asc' : 'desc') ->orderBy($this->sort_field, $this->sort_asc ? 'asc' : 'desc')
->paginate($this->per_page); ->paginate($this->per_page);

View File

@ -30,6 +30,7 @@ class InvoicesTable extends Component
public function render() public function render()
{ {
$query = Invoice::query() $query = Invoice::query()
->orderBy($this->sort_field, $this->sort_asc ? 'asc' : 'desc'); ->orderBy($this->sort_field, $this->sort_asc ? 'asc' : 'desc');
@ -48,7 +49,7 @@ class InvoicesTable extends Component
} }
$query = $query $query = $query
->where('company_id', auth('contact')->user()->company->id) ->where('client_id', auth('contact')->user()->client->id)
->paginate($this->per_page); ->paginate($this->per_page);
return render('components.livewire.invoices-table', [ return render('components.livewire.invoices-table', [

View File

@ -24,7 +24,6 @@ class PaymentMethodsTable extends Component
{ {
$query = ClientGatewayToken::query() $query = ClientGatewayToken::query()
->with('gateway_type') ->with('gateway_type')
->where('company_id', auth('contact')->user()->company->id)
->where('client_id', $this->client->id) ->where('client_id', $this->client->id)
->orderBy($this->sort_field, $this->sort_asc ? 'asc' : 'desc') ->orderBy($this->sort_field, $this->sort_asc ? 'asc' : 'desc')
->paginate($this->per_page); ->paginate($this->per_page);

View File

@ -24,7 +24,7 @@ class PaymentsTable extends Component
{ {
$query = Payment::query() $query = Payment::query()
->with('type', 'client') ->with('type', 'client')
->where('company_id', auth('contact')->user()->company->id) ->where('client_id', auth('contact')->user()->client->id)
->orderBy($this->sort_field, $this->sort_asc ? 'asc' : 'desc') ->orderBy($this->sort_field, $this->sort_asc ? 'asc' : 'desc')
->paginate($this->per_page); ->paginate($this->per_page);

View File

@ -46,7 +46,7 @@ class QuotesTable extends Component
} }
$query = $query $query = $query
->where('company_id', auth('contact')->user()->company->id) ->where('client_id', auth('contact')->user()->client->id)
->paginate($this->per_page); ->paginate($this->per_page);
return render('components.livewire.quotes-table', [ return render('components.livewire.quotes-table', [

View File

@ -18,7 +18,7 @@ class RecurringInvoicesTable extends Component
$query = RecurringInvoice::query(); $query = RecurringInvoice::query();
$query = $query $query = $query
->where('company_id', auth('contact')->user()->company->id) ->where('client_id', auth('contact')->user()->client->id)
->whereIn('status_id', [RecurringInvoice::STATUS_PENDING, RecurringInvoice::STATUS_ACTIVE, RecurringInvoice::STATUS_COMPLETED]) ->whereIn('status_id', [RecurringInvoice::STATUS_PENDING, RecurringInvoice::STATUS_ACTIVE, RecurringInvoice::STATUS_COMPLETED])
->orderBy('status_id', 'asc') ->orderBy('status_id', 'asc')
->with('client') ->with('client')

View File

@ -37,6 +37,7 @@ class Cors
$response->headers->set('Access-Control-Allow-Origin', '*'); $response->headers->set('Access-Control-Allow-Origin', '*');
$response->headers->set('Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE, OPTIONS'); $response->headers->set('Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE, OPTIONS');
$response->headers->set('Access-Control-Allow-Headers', 'X-API-SECRET,X-API-TOKEN,X-API-PASSWORD,DNT,User-Agent,X-Requested-With,If-Modified-Since,Cache-Control,Content-Type,Range'); $response->headers->set('Access-Control-Allow-Headers', 'X-API-SECRET,X-API-TOKEN,X-API-PASSWORD,DNT,User-Agent,X-Requested-With,If-Modified-Since,Cache-Control,Content-Type,Range');
$response->headers->set('Access-Control-Expose-Headers', 'X-APP-VERSION');
$response->headers->set('X-APP-VERSION', config('ninja.app_version')); $response->headers->set('X-APP-VERSION', config('ninja.app_version'));
$response->headers->set('X-API-VERSION', config('ninja.api_version')); $response->headers->set('X-API-VERSION', config('ninja.api_version'));

View File

@ -50,8 +50,8 @@ class QueryLogging
Log::info($request->method() . ' - ' . $request->url() . ": $count queries - " . $time); Log::info($request->method() . ' - ' . $request->url() . ": $count queries - " . $time);
// if($count > 50) //if($count > 10)
// Log::info($queries); // Log::info($queries);
} }
} }

View File

@ -12,7 +12,6 @@
namespace App\Http\Requests\Company; namespace App\Http\Requests\Company;
use App\Http\Requests\Request; use App\Http\Requests\Request;
use App\Models\Company;
class DestroyCompanyRequest extends Request class DestroyCompanyRequest extends Request
{ {

View File

@ -67,6 +67,10 @@ class StoreInvoiceRequest extends Request
$input['client_id'] = $this->decodePrimaryKey($input['client_id']); $input['client_id'] = $this->decodePrimaryKey($input['client_id']);
} }
if (array_key_exists('assigned_user_id', $input) && is_string($input['assigned_user_id'])) {
$input['assigned_user_id'] = $this->decodePrimaryKey($input['assigned_user_id']);
}
if (isset($input['client_contacts'])) { if (isset($input['client_contacts'])) {
foreach ($input['client_contacts'] as $key => $contact) { foreach ($input['client_contacts'] as $key => $contact) {
if (!array_key_exists('send_email', $contact) || !array_key_exists('id', $contact)) { if (!array_key_exists('send_email', $contact) || !array_key_exists('id', $contact)) {

View File

@ -65,6 +65,10 @@ class UpdateInvoiceRequest extends Request
$input['client_id'] = $this->decodePrimaryKey($input['client_id']); $input['client_id'] = $this->decodePrimaryKey($input['client_id']);
} }
if (array_key_exists('assigned_user_id', $input) && is_string($input['assigned_user_id'])) {
$input['assigned_user_id'] = $this->decodePrimaryKey($input['assigned_user_id']);
}
if (isset($input['invitations'])) { if (isset($input['invitations'])) {
foreach ($input['invitations'] as $key => $value) { foreach ($input['invitations'] as $key => $value) {
if (is_numeric($input['invitations'][$key]['id'])) { if (is_numeric($input['invitations'][$key]['id'])) {

View File

@ -35,9 +35,10 @@ class UpdatePaymentRequest extends Request
public function rules() public function rules()
{ {//min:1 removed
return [ return [
'invoices' => ['required','array','min:1',new PaymentAppliedValidAmount,new ValidCreditsPresentRule], 'invoices' => ['required','array',new PaymentAppliedValidAmount,new ValidCreditsPresentRule],
'invoices.*.invoice_id' => 'distinct',
'documents' => 'mimes:png,ai,svg,jpeg,tiff,pdf,gif,psd,txt,doc,xls,ppt,xlsx,docx,pptx', 'documents' => 'mimes:png,ai,svg,jpeg,tiff,pdf,gif,psd,txt,doc,xls,ppt,xlsx,docx,pptx',
]; ];
} }
@ -77,4 +78,11 @@ class UpdatePaymentRequest extends Request
} }
$this->replace($input); $this->replace($input);
} }
public function messages()
{
return [
'distinct' => 'Attemping duplicate payment on the same invoice Invoice',
];
}
} }

View File

@ -49,6 +49,7 @@ class PortalComposer
$data['company'] = auth()->user()->company; $data['company'] = auth()->user()->company;
$data['client'] = auth()->user()->client; $data['client'] = auth()->user()->client;
$data['settings'] = auth()->user()->client->getMergedSettings(); $data['settings'] = auth()->user()->client->getMergedSettings();
$data['currencies'] = TranslationHelper::getCurrencies();
$data['multiple_contacts'] = ClientContact::where('email', auth('contact')->user()->email)->whereNotNull('email')->distinct('company_id')->get(); $data['multiple_contacts'] = ClientContact::where('email', auth('contact')->user()->email)->whereNotNull('email')->distinct('company_id')->get();
@ -63,9 +64,9 @@ class PortalComposer
$data[] = [ 'title' => ctrans('texts.invoices'), 'url' => 'client.invoices.index', 'icon' => 'file-text']; $data[] = [ 'title' => ctrans('texts.invoices'), 'url' => 'client.invoices.index', 'icon' => 'file-text'];
$data[] = [ 'title' => ctrans('texts.recurring_invoices'), 'url' => 'client.recurring_invoices.index', 'icon' => 'file']; $data[] = [ 'title' => ctrans('texts.recurring_invoices'), 'url' => 'client.recurring_invoices.index', 'icon' => 'file'];
$data[] = [ 'title' => ctrans('texts.payments'), 'url' => 'client.payments.index', 'icon' => 'credit-card']; $data[] = [ 'title' => ctrans('texts.payments'), 'url' => 'client.payments.index', 'icon' => 'credit-card'];
$data[] = [ 'title' => ctrans('texts.payment_methods'), 'url' => 'client.payment_methods.index', 'icon' => 'shield'];
$data[] = [ 'title' => ctrans('texts.quotes'), 'url' => 'client.quotes.index', 'icon' => 'align-left']; $data[] = [ 'title' => ctrans('texts.quotes'), 'url' => 'client.quotes.index', 'icon' => 'align-left'];
$data[] = [ 'title' => ctrans('texts.credits'), 'url' => 'client.credits.index', 'icon' => 'credit-card']; $data[] = [ 'title' => ctrans('texts.credits'), 'url' => 'client.credits.index', 'icon' => 'credit-card'];
$data[] = [ 'title' => ctrans('texts.payment_methods'), 'url' => 'client.payment_methods.index', 'icon' => 'shield'];
return $data; return $data;
} }

View File

@ -50,14 +50,14 @@ class CreateCompanyPaymentTerms
{ {
$paymentTerms = [ $paymentTerms = [
['num_days' => 0, 'name' => 'Net 0', 'company_id' => $this->company->id, 'user_id' => $this->user->id], ['num_days' => 0, 'name' => 'Net 0', 'company_id' => $this->company->id, 'user_id' => $this->user->id, 'created_at' => now(), 'updated_at' => now()],
['num_days' => 7, 'name' => '', 'company_id' => $this->company->id, 'user_id' => $this->user->id], ['num_days' => 7, 'name' => '', 'company_id' => $this->company->id, 'user_id' => $this->user->id, 'created_at' => now(), 'updated_at' => now()],
['num_days' => 10, 'name' => '', 'company_id' => $this->company->id, 'user_id' => $this->user->id], ['num_days' => 10, 'name' => '', 'company_id' => $this->company->id, 'user_id' => $this->user->id, 'created_at' => now(), 'updated_at' => now()],
['num_days' => 14, 'name' => '', 'company_id' => $this->company->id, 'user_id' => $this->user->id], ['num_days' => 14, 'name' => '', 'company_id' => $this->company->id, 'user_id' => $this->user->id, 'created_at' => now(), 'updated_at' => now()],
['num_days' => 15, 'name' => '', 'company_id' => $this->company->id, 'user_id' => $this->user->id], ['num_days' => 15, 'name' => '', 'company_id' => $this->company->id, 'user_id' => $this->user->id, 'created_at' => now(), 'updated_at' => now()],
['num_days' => 30, 'name' => '', 'company_id' => $this->company->id, 'user_id' => $this->user->id], ['num_days' => 30, 'name' => '', 'company_id' => $this->company->id, 'user_id' => $this->user->id, 'created_at' => now(), 'updated_at' => now()],
['num_days' => 60, 'name' => '', 'company_id' => $this->company->id, 'user_id' => $this->user->id], ['num_days' => 60, 'name' => '', 'company_id' => $this->company->id, 'user_id' => $this->user->id, 'created_at' => now(), 'updated_at' => now()],
['num_days' => 90, 'name' => '', 'company_id' => $this->company->id, 'user_id' => $this->user->id], ['num_days' => 90, 'name' => '', 'company_id' => $this->company->id, 'user_id' => $this->user->id, 'created_at' => now(), 'updated_at' => now()],
]; ];
PaymentTerm::insert($paymentTerms); PaymentTerm::insert($paymentTerms);

View File

@ -0,0 +1,56 @@
<?php
namespace App\Jobs\Invoice;
use App\Models\Invoice;
use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Bus\Dispatchable;
use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Queue\SerializesModels;
class InjectSignature implements ShouldQueue
{
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
/**
* @var App\Models\Invoice
*/
public $invoice;
/**
* @var string
*/
public $signature;
/**
* Create a new job instance.
*
* @return void
*/
public function __construct(Invoice $invoice, string $signature)
{
$this->invoice = $invoice;
$this->signature = $signature;
}
/**
* Execute the job.
*
* @return void
*/
public function handle()
{
$invitation = $this->invoice->invitations->whereNotNull('signature_base64')->first();
if (!$invitation) {
return;
}
$invitation->signature_base64 = $this->signature;
$invitation->save();
CreateInvoicePdf::dispatch($invitation);
}
}

View File

@ -0,0 +1,54 @@
<?php
/**
* Invoice Ninja (https://invoiceninja.com)
*
* @link https://github.com/invoiceninja/invoiceninja source repository
*
* @copyright Copyright (c) 2020. Invoice Ninja LLC (https://invoiceninja.com)
*
* @license https://opensource.org/licenses/AAL
*/
namespace App\Jobs\Ninja;
use App\Helpers\Email\InvoiceEmail;
use App\Jobs\Invoice\EmailInvoice;
use App\Libraries\MultiDB;
use App\Models\Account;
use App\Models\SystemLog;
use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Bus\Dispatchable;
use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Queue\SerializesModels;
use Turbo124\Beacon\Jobs\Database\MySQL\DbStatus;
class CheckDbStatus implements ShouldQueue
{
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
/**
* Create a new job instance.
*
* @return void
*/
public function __construct()
{
//
}
/**
* Execute the job.
*
* @return void
*/
public function handle()
{
DbStatus::dispatchNow('db-ninja-01', 'db.status.db-ninja-01');
DbStatus::dispatchNow('db-ninja-02', 'db.status.db-ninja-02');
}
}

View File

@ -37,7 +37,7 @@ class RefundCancelledAccount implements ShouldQueue
if(Ninja::isSelfHost() || $this->account->isFreeHostedClient()) if(Ninja::isSelfHost() || $this->account->isFreeHostedClient())
return; return;
$plan_details = $account->getPlanDetails(); $plan_details = $this->account->getPlanDetails();
/* Trial user cancelling early.... */ /* Trial user cancelling early.... */
if($plan_details['trial_active']) if($plan_details['trial_active'])

View File

@ -43,7 +43,7 @@ class SubscriptionHandler implements ShouldQueue
if(!$this->entity->company || $this->entity->company->company_users->first()->is_migrating) if(!$this->entity->company || $this->entity->company->company_users->first()->is_migrating)
return true; return true;
info("i got past the check"); //info("i got past the check");
$subscriptions = Subscription::where('company_id', $this->entity->company_id) $subscriptions = Subscription::where('company_id', $this->entity->company_id)
->where('event_id', $this->event_id) ->where('event_id', $this->event_id)

View File

@ -47,6 +47,9 @@ class UploadAvatar implements ShouldQueue
$path = Storage::putFile('public/' . $this->directory, new File(sys_get_temp_dir().'/'.$tmp_file)); $path = Storage::putFile('public/' . $this->directory, new File(sys_get_temp_dir().'/'.$tmp_file));
info($path);
info($tmp_file);
$url = Storage::url($path); $url = Storage::url($path);
//return file path //return file path

View File

@ -48,6 +48,6 @@ class CreateInvoiceActivity implements ShouldQueue
$fields->company_id = $event->invoice->company_id; $fields->company_id = $event->invoice->company_id;
$fields->activity_type_id = Activity::CREATE_INVOICE; $fields->activity_type_id = Activity::CREATE_INVOICE;
$this->activity_repo->save($fields, $event->invoice); $this->activity_repo->save($fields, $event->invoice, $event->invoice->company->db);
} }
} }

View File

@ -35,6 +35,10 @@ class CreateInvoicePdf implements ShouldQueue
*/ */
public function handle($event) public function handle($event)
{ {
PdfCreator::dispatch($event->invoice->invitations->first()); $event->invoice->invitations->each(function ($invitation) {
PdfCreator::dispatch($invitation);
});
} }
} }

View File

@ -50,7 +50,8 @@ class UpdateInvoiceActivity implements ShouldQueue
$fields->user_id = $event->invoice->user_id; $fields->user_id = $event->invoice->user_id;
$fields->company_id = $event->invoice->company_id; $fields->company_id = $event->invoice->company_id;
$fields->activity_type_id = Activity::UPDATE_INVOICE; $fields->activity_type_id = Activity::UPDATE_INVOICE;
$fields->invoice_id = $event->invoice->id;
$this->activity_repo->save($fields, $event->invoice); $this->activity_repo->save($fields, $event->invoice);
} }
} }

View File

@ -251,12 +251,12 @@ class Client extends BaseModel implements HasLocalePreference
* @param float $amount Adjustment amount * @param float $amount Adjustment amount
* @return Client * @return Client
*/ */
public function processUnappliedPayment($amount) :Client // public function processUnappliedPayment($amount) :Client
{ // {
return $this->service()->updatePaidToDate($amount) // return $this->service()->updatePaidToDate($amount)
->adjustCreditBalance($amount) // ->adjustCreditBalance($amount)
->save(); // ->save();
} // }
/** /**
* *
@ -507,4 +507,9 @@ class Client extends BaseModel implements HasLocalePreference
return $defaults; return $defaults;
} }
public function payments()
{
return $this->hasMany(Payment::class);
}
} }

View File

@ -29,6 +29,9 @@ class ClientGatewayToken extends BaseModel
'deleted_at' => 'timestamp', 'deleted_at' => 'timestamp',
]; ];
protected $appends = [
'hashed_id',
];
public function getEntityType() public function getEntityType()
{ {
return ClientGatewayToken::class; return ClientGatewayToken::class;

View File

@ -17,9 +17,12 @@ use App\Models\Gateway;
use App\Models\GatewayType; use App\Models\GatewayType;
use App\Utils\Number; use App\Utils\Number;
use Illuminate\Database\Eloquent\Model; use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\SoftDeletes;
class CompanyGateway extends BaseModel class CompanyGateway extends BaseModel
{ {
use SoftDeletes;
protected $casts = [ protected $casts = [
'fees_and_limits' => 'object', 'fees_and_limits' => 'object',
'updated_at' => 'timestamp', 'updated_at' => 'timestamp',

View File

@ -70,6 +70,7 @@ class Credit extends BaseModel
protected $casts = [ protected $casts = [
'line_items' => 'object', 'line_items' => 'object',
'backup' => 'object',
'updated_at' => 'timestamp', 'updated_at' => 'timestamp',
'created_at' => 'timestamp', 'created_at' => 'timestamp',
'deleted_at' => 'timestamp', 'deleted_at' => 'timestamp',

View File

@ -25,5 +25,7 @@ class Currency extends StaticModel
'updated_at' => 'timestamp', 'updated_at' => 'timestamp',
'created_at' => 'timestamp', 'created_at' => 'timestamp',
'deleted_at' => 'timestamp', 'deleted_at' => 'timestamp',
//'precision' => 'string',
'precision' => 'integer',
]; ];
} }

View File

@ -19,6 +19,7 @@ use App\Helpers\Invoice\InvoiceSumInclusive;
use App\Jobs\Client\UpdateClientBalance; use App\Jobs\Client\UpdateClientBalance;
use App\Jobs\Company\UpdateCompanyLedgerWithInvoice; use App\Jobs\Company\UpdateCompanyLedgerWithInvoice;
use App\Jobs\Invoice\CreateInvoicePdf; use App\Jobs\Invoice\CreateInvoicePdf;
use App\Models\Backup;
use App\Models\CompanyLedger; use App\Models\CompanyLedger;
use App\Models\Currency; use App\Models\Currency;
use App\Models\Filterable; use App\Models\Filterable;
@ -98,10 +99,12 @@ class Invoice extends BaseModel
'custom_surcharge_tax3', 'custom_surcharge_tax3',
'custom_surcharge_tax4', 'custom_surcharge_tax4',
'design_id', 'design_id',
'assigned_user_id',
]; ];
protected $casts = [ protected $casts = [
'line_items' => 'object', 'line_items' => 'object',
'backup' => 'object',
'updated_at' => 'timestamp', 'updated_at' => 'timestamp',
'created_at' => 'timestamp', 'created_at' => 'timestamp',
'deleted_at' => 'timestamp', 'deleted_at' => 'timestamp',
@ -208,7 +211,7 @@ class Invoice extends BaseModel
public function history() public function history()
{ {
$this->activities->with('backup'); return $this->hasManyThrough(Backup::class, Activity::class);
} }
// public function credits() // public function credits()

View File

@ -201,7 +201,9 @@ class Payment extends BaseModel
public function refund(array $data) :Payment public function refund(array $data) :Payment
{ {
return $this->processRefund($data); return $this->service()->refundPayment($data);
//return $this->processRefund($data);
} }
/** /**

View File

@ -25,4 +25,14 @@ class ClientContactPresenter extends EntityPresenter
{ {
return $this->entity->first_name . ' ' . $this->entity->last_name; return $this->entity->first_name . ' ' . $this->entity->last_name;
} }
public function first_name()
{
return $this->entity->first_name ?: '';
}
public function last_name()
{
return $this->entity->last_name ?: '';
}
} }

View File

@ -25,15 +25,20 @@ class ClientPresenter extends EntityPresenter
*/ */
public function name() public function name()
{ {
if($this->entity->name)
return $this->entity->name;
$contact = $this->entity->primary_contact->first(); $contact = $this->entity->primary_contact->first();
$contact_name = 'No Contact Set'; $contact_name = 'No Contact Set';
if ($contact) { if ($contact && (strlen($contact->first_name) >=1 || strlen($contact->last_name) >=1)) {
$contact_name = $contact->first_name. ' '. $contact->last_name; $contact_name = $contact->first_name. ' '. $contact->last_name;
} }
elseif($contact && (strlen($contact->email)))
$contact_name = $contact->email;
return $this->entity->name ?: $contact_name; return $contact_name;
} }
public function primary_contact_name() public function primary_contact_name()

View File

@ -38,7 +38,7 @@ class CompanyPresenter extends EntityPresenter
$settings = $this->entity->settings; $settings = $this->entity->settings;
} }
return iconv_strlen($settings->company_logo > 0) ? $settings->company_logo : 'https://www.invoiceninja.com/wp-content/uploads/2019/01/InvoiceNinja-Logo-Round-300x300.png'; return (strlen($settings->company_logo) > 0) ? $settings->company_logo : 'https://www.invoiceninja.com/wp-content/uploads/2019/01/InvoiceNinja-Logo-Round-300x300.png';
} }
public function address($settings = null) public function address($settings = null)

View File

@ -76,6 +76,7 @@ class Quote extends BaseModel
'due_date' => 'date:Y-m-d', 'due_date' => 'date:Y-m-d',
'partial_due_date' => 'date:Y-m-d', 'partial_due_date' => 'date:Y-m-d',
'line_items' => 'object', 'line_items' => 'object',
'backup' => 'object',
'updated_at' => 'timestamp', 'updated_at' => 'timestamp',
'created_at' => 'timestamp', 'created_at' => 'timestamp',
'deleted_at' => 'timestamp', 'deleted_at' => 'timestamp',
@ -84,6 +85,7 @@ class Quote extends BaseModel
const STATUS_DRAFT = 1; const STATUS_DRAFT = 1;
const STATUS_SENT = 2; const STATUS_SENT = 2;
const STATUS_APPROVED = 3; const STATUS_APPROVED = 3;
const STATUS_CONVERTED = 4;
const STATUS_EXPIRED = -1; const STATUS_EXPIRED = -1;
public function getEntityType() public function getEntityType()

View File

@ -101,6 +101,7 @@ class RecurringInvoice extends BaseModel
protected $casts = [ protected $casts = [
'settings' => 'object', 'settings' => 'object',
'line_items' => 'object', 'line_items' => 'object',
'backup' => 'object',
'updated_at' => 'timestamp', 'updated_at' => 'timestamp',
'created_at' => 'timestamp', 'created_at' => 'timestamp',
'deleted_at' => 'timestamp', 'deleted_at' => 'timestamp',

View File

@ -83,6 +83,7 @@ class RecurringQuote extends BaseModel
protected $casts = [ protected $casts = [
'line_items' => 'object', 'line_items' => 'object',
'backup' => 'object',
'settings' => 'object', 'settings' => 'object',
'updated_at' => 'timestamp', 'updated_at' => 'timestamp',
'created_at' => 'timestamp', 'created_at' => 'timestamp',

View File

@ -35,6 +35,8 @@ class SystemLog extends Model
const TYPE_STRIPE = 301; const TYPE_STRIPE = 301;
const TYPE_LEDGER = 302; const TYPE_LEDGER = 302;
const TYPE_FAILURE = 303; const TYPE_FAILURE = 303;
const TYPE_CHECKOUT = 304;
const TYPE_AUTHORIZE = 305;
const TYPE_QUOTA_EXCEEDED = 400; const TYPE_QUOTA_EXCEEDED = 400;
const TYPE_UPSTREAM_FAILURE = 401; const TYPE_UPSTREAM_FAILURE = 401;

View File

@ -0,0 +1,25 @@
<?php
/**
* Invoice Ninja (https://invoiceninja.com)
*
* @link https://github.com/invoiceninja/invoiceninja source repository
*
* @copyright Copyright (c) 2020. Invoice Ninja LLC (https://invoiceninja.com)
*
* @license https://opensource.org/licenses/AAL
*/
namespace App\PaymentDrivers;
abstract class AbstractPaymentDriver
{
abstract public function authorize($payment_method);
abstract public function purchase($amount, $return_client_response = false);
abstract public function refund($amount, $transaction_reference, $return_client_response = false);
abstract public function setPaymentMethod($payment_method_id);
}

View File

@ -0,0 +1,89 @@
<?php
/**
* Invoice Ninja (https://invoiceninja.com)
*
* @link https://github.com/invoiceninja/invoiceninja source repository
*
* @copyright Copyright (c) 2020. Invoice Ninja LLC (https://invoiceninja.com)
*
* @license https://opensource.org/licenses/AAL
*/
namespace App\PaymentDrivers\Authorize;
use App\Exceptions\GenericPaymentDriverFailure;
use App\Models\Client;
use App\Models\GatewayType;
use App\PaymentDrivers\AuthorizePaymentDriver;
use net\authorize\api\constants\ANetEnvironment;
use net\authorize\api\contract\v1\CreateCustomerProfileRequest;
use net\authorize\api\contract\v1\CustomerAddressType;
use net\authorize\api\contract\v1\CustomerPaymentProfileType;
use net\authorize\api\contract\v1\CustomerProfileType;
use net\authorize\api\controller\CreateCustomerProfileController;
/**
* Class BaseDriver
* @package App\PaymentDrivers
*
*/
class AuthorizeCreateCustomer
{
public $authorize;
public $client;
public function __construct(AuthorizePaymentDriver $authorize, Client $client)
{
$this->authorize = $authorize;
$this->client = $client;
}
public function create($data = null)
{
error_reporting (E_ALL & ~E_DEPRECATED);
$this->authorize->init();
// Create the Bill To info for new payment type
$contact = $this->client->primary_contact()->first();
$refId = 'ref' . time();
// Create a new CustomerProfileType and add the payment profile object
$customerProfile = new CustomerProfileType();
$customerProfile->setDescription($this->client->present()->name());
$customerProfile->setMerchantCustomerId("M_" . time());
$customerProfile->setEmail($this->client->present()->email());
// Assemble the complete transaction request
$request = new CreateCustomerProfileRequest();
$request->setMerchantAuthentication($this->authorize->merchant_authentication);
$request->setRefId($refId);
$request->setProfile($customerProfile);
// Create the controller and get the response
$controller = new CreateCustomerProfileController($request);
$response = $controller->executeWithApiResponse($this->authorize->mode());
if (($response != null) && ($response->getMessages()->getResultCode() == "Ok")) {
return $response->getCustomerProfileId();
} else {
$errorMessages = $response->getMessages()->getMessage();
$message = "Unable to add customer to Authorize.net gateway";
if(is_array($errorMessages))
$message = $errorMessages[0]->getCode() . " " .$errorMessages[0]->getText();
throw new GenericPaymentDriverFailure($message);
}
}
}

View File

@ -0,0 +1,170 @@
<?php
/**
* Invoice Ninja (https://invoiceninja.com)
*
* @link https://github.com/invoiceninja/invoiceninja source repository
*
* @copyright Copyright (c) 2020. Invoice Ninja LLC (https://invoiceninja.com)
*
* @license https://opensource.org/licenses/AAL
*/
namespace App\PaymentDrivers\Authorize;
use App\Events\Payment\PaymentWasCreated;
use App\Factory\PaymentFactory;
use App\Jobs\Util\SystemLogger;
use App\Models\ClientGatewayToken;
use App\Models\GatewayType;
use App\Models\Payment;
use App\Models\PaymentType;
use App\Models\SystemLog;
use App\PaymentDrivers\AuthorizePaymentDriver;
use App\PaymentDrivers\Authorize\AuthorizeCreateCustomer;
use App\PaymentDrivers\Authorize\AuthorizePaymentMethod;
use App\PaymentDrivers\Authorize\ChargePaymentProfile;
use App\Utils\Traits\MakesHash;
use Illuminate\Support\Carbon;
/**
* Class AuthorizeCreditCard
* @package App\PaymentDrivers\Authorize
*
*/
class AuthorizeCreditCard
{
use MakesHash;
public $authorize;
public function __construct(AuthorizePaymentDriver $authorize)
{
$this->authorize = $authorize;
}
public function processPaymentView($data)
{
$tokens = ClientGatewayToken::where('client_id', $this->authorize->client->id)
->where('company_gateway_id', $this->authorize->company_gateway->id)
->where('gateway_type_id', GatewayType::CREDIT_CARD)
->get();
$data['tokens'] = $tokens;
$data['gateway'] = $this->authorize->company_gateway;
$data['public_client_id'] = $this->authorize->init()->getPublicClientKey();
$data['api_login_id'] = $this->authorize->company_gateway->getConfigField('apiLoginId');
return render('gateways.authorize.credit_card_payment', $data);
}
public function processPaymentResponse($request)
{
if($request->token)
return $this->processTokenPayment($request);
$data = $request->all();
$authorise_create_customer = new AuthorizeCreateCustomer($this->authorize, $this->authorize->client);
$gateway_customer_reference = $authorise_create_customer->create($data);
info($gateway_customer_reference);
$authorise_payment_method = new AuthorizePaymentMethod($this->authorize);
$payment_profile = $authorise_payment_method->addPaymentMethodToClient($gateway_customer_reference, $data);
$payment_profile_id = $payment_profile->getPaymentProfile()->getCustomerPaymentProfileId();
info($request->input('store_card'));
if($request->has('store_card') && $request->input('store_card') === 'true'){
$authorise_payment_method->payment_method = GatewayType::CREDIT_CARD;
$client_gateway_token = $authorise_payment_method->createClientGatewayToken($payment_profile, $gateway_customer_reference);
}
$data = (new ChargePaymentProfile($this->authorize))->chargeCustomerProfile($gateway_customer_reference, $payment_profile_id, $data['amount_with_fee']);
return $this->handleResponse($data, $request);
}
private function processTokenPayment($request)
{
$client_gateway_token = ClientGatewayToken::find($this->decodePrimaryKey($request->token));
$data = (new ChargePaymentProfile($this->authorize))->chargeCustomerProfile($client_gateway_token->gateway_customer_reference, $client_gateway_token->token, $request->input('amount_with_fee'));
return $this->handleResponse($data, $request);
}
private function handleResponse($data, $request)
{
//info(print_r( $response->getTransactionResponse()->getMessages(),1));
$response = $data['response'];
if($response != null && $response->getMessages()->getResultCode() == "Ok")
return $this->processSuccessfulResponse($data, $request);
return $this->processFailedResponse($data, $request);
}
private function processSuccessfulResponse($data, $request)
{
$response = $data['response'];
//create a payment record and fire notifications and then return
$payment = PaymentFactory::create($this->authorize->client->company_id, $this->authorize->client->user_id);
$payment->client_id = $this->authorize->client->id;
$payment->company_gateway_id = $this->authorize->company_gateway->id;
$payment->status_id = Payment::STATUS_COMPLETED;
$payment->type_id = PaymentType::CREDIT_CARD_OTHER;
$payment->currency_id = $this->authorize->client->getSetting('currency_id');
$payment->date = Carbon::now();
$payment->transaction_reference = $response->getTransactionResponse()->getTransId();
$payment->amount = $request->input('amount_with_fee');
$payment->currency_id = $this->authorize->client->getSetting('currency_id');
$payment->client->getNextPaymentNumber($this->authorize->client);
$payment->save();
$this->authorize->attachInvoices($payment, $request->hashed_ids);
$payment->service()->updateInvoicePayment();
event(new PaymentWasCreated($payment, $payment->company));
$logger_message = [
'server_response' => $response->getTransactionResponse()->getTransId(),
'data' => $this->formatGatewayResponse($data, $request)
];
SystemLogger::dispatch($logger_message, SystemLog::CATEGORY_GATEWAY_RESPONSE, SystemLog::EVENT_GATEWAY_SUCCESS, SystemLog::TYPE_AUTHORIZE, $this->authorize->client);
return redirect()->route('client.payments.show', ['payment' => $this->encodePrimaryKey($payment->id)]);
}
private function processFailedResponse($data, $request)
{ dd($data);
info(print_r($data,1));
}
private function formatGatewayResponse($data, $request)
{
$response = $data['response'];
return [
'transaction_reference' => $response->getTransactionResponse()->getTransId(),
'amount' => $request->input('amount'),
'auth_code' => $response->getTransactionResponse()->getAuthCode(),
'code' => $response->getTransactionResponse()->getMessages()[0]->getCode(),
'description' => $response->getTransactionResponse()->getMessages()[0]->getDescription(),
'invoices' => $request->hashed_ids,
];
}
}

View File

@ -0,0 +1,267 @@
<?php
/**
* Invoice Ninja (https://invoiceninja.com)
*
* @link https://github.com/invoiceninja/invoiceninja source repository
*
* @copyright Copyright (c) 2020. Invoice Ninja LLC (https://invoiceninja.com)
*
* @license https://opensource.org/licenses/AAL
*/
namespace App\PaymentDrivers\Authorize;
use App\Exceptions\GenericPaymentDriverFailure;
use App\Models\ClientGatewayToken;
use App\Models\GatewayType;
use App\PaymentDrivers\AuthorizePaymentDriver;
use App\PaymentDrivers\Authorize\AuthorizeCreateCustomer;
use net\authorize\api\contract\v1\CreateCustomerPaymentProfileRequest;
use net\authorize\api\contract\v1\CreateCustomerProfileRequest;
use net\authorize\api\contract\v1\CustomerAddressType;
use net\authorize\api\contract\v1\CustomerPaymentProfileType;
use net\authorize\api\contract\v1\CustomerProfileType;
use net\authorize\api\contract\v1\GetCustomerPaymentProfileRequest;
use net\authorize\api\contract\v1\OpaqueDataType;
use net\authorize\api\contract\v1\PaymentType;
use net\authorize\api\controller\CreateCustomerPaymentProfileController;
use net\authorize\api\controller\CreateCustomerProfileController;
use net\authorize\api\controller\GetCustomerPaymentProfileController;
/**
* Class AuthorizePaymentMethod
* @package App\PaymentDrivers\AuthorizePaymentMethod
*
*/
class AuthorizePaymentMethod
{
public $authorize;
public $payment_method;
public function __construct(AuthorizePaymentDriver $authorize)
{
$this->authorize = $authorize;
}
public function authorizeView($payment_method)
{
$this->payment_method = $payment_method;
switch ($payment_method) {
case GatewayType::CREDIT_CARD:
return $this->authorizeCreditCard();
break;
case GatewayType::BANK_TRANSFER:
return $this->authorizeBankTransfer();
break;
default:
# code...
break;
}
}
public function authorizeResponseView($payment_method, $data)
{
$this->payment_method = $payment_method;
switch ($payment_method) {
case GatewayType::CREDIT_CARD:
return $this->authorizeCreditCardResponse($data);
break;
case GatewayType::BANK_TRANSFER:
return $this->authorizeBankTransferResponse($data);
break;
default:
# code...
break;
}
}
public function authorizeCreditCard()
{
$data['gateway'] = $this->authorize->company_gateway;
$data['public_client_id'] = $this->authorize->init()->getPublicClientKey();
$data['api_login_id'] = $this->authorize->company_gateway->getConfigField('apiLoginId');
return render('gateways.authorize.add_credit_card', $data);
}
public function authorizeBankTransfer()
{
}
public function authorizeCreditCardResponse($data)
{
$client_profile_id = null;
if($client_gateway_token = $this->authorize->findClientGatewayRecord()){
$payment_profile = $this->addPaymentMethodToClient($client_gateway_token->gateway_customer_reference, $data);
}
else{
$gateway_customer_reference = (new AuthorizeCreateCustomer($this->authorize, $this->authorize->client))->create($data);
$payment_profile = $this->addPaymentMethodToClient($gateway_customer_reference, $data);
}
$this->createClientGatewayToken($payment_profile, $gateway_customer_reference);
return redirect()->route('client.payment_methods.index');
}
public function authorizeBankTransferResponse($data)
{
}
public function createClientGatewayToken($payment_profile, $gateway_customer_reference)
{
info(print_r($payment_profile,1));
$client_gateway_token = new ClientGatewayToken();
$client_gateway_token->company_id = $this->authorize->client->company_id;
$client_gateway_token->client_id = $this->authorize->client->id;
$client_gateway_token->token = $payment_profile->getPaymentProfile()->getCustomerPaymentProfileId();
$client_gateway_token->company_gateway_id = $this->authorize->company_gateway->id;
$client_gateway_token->gateway_type_id = $this->payment_method;
$client_gateway_token->gateway_customer_reference = $gateway_customer_reference;
$client_gateway_token->meta = $this->buildPaymentMethod($payment_profile);
$client_gateway_token->save();
return $client_gateway_token;
}
public function buildPaymentMethod($payment_profile)
{
$payment_meta = new \stdClass;
$payment_meta->exp_month = 'xx';
$payment_meta->exp_year = 'xx';
$payment_meta->brand = $payment_profile->getPaymentProfile()->getPayment()->getCreditCard()->getCardType();
$payment_meta->last4 = $payment_profile->getPaymentProfile()->getPayment()->getCreditCard()->getCardNumber();
$payment_meta->type = $this->payment_method;
return $payment_meta;
}
public function addPaymentMethodToClient($gateway_customer_reference, $data)
{
error_reporting (E_ALL & ~E_DEPRECATED);
$this->authorize->init();
// Set the transaction's refId
$refId = 'ref' . time();
// Set the payment data for the payment profile to a token obtained from Accept.js
$op = new OpaqueDataType();
$op->setDataDescriptor($data['dataDescriptor']);
$op->setDataValue($data['dataValue']);
$paymentOne = new PaymentType();
$paymentOne->setOpaqueData($op);
$contact = $this->authorize->client->primary_contact()->first();
if($contact){
// Create the Bill To info for new payment type
$billto = new CustomerAddressType();
$billto->setFirstName($contact->present()->first_name());
$billto->setLastName($contact->present()->last_name());
$billto->setCompany($this->authorize->client->present()->name());
$billto->setAddress($this->authorize->client->address1);
$billto->setCity($this->authorize->client->city);
$billto->setState($this->authorize->client->state);
$billto->setZip($this->authorize->client->postal_code);
if($this->authorize->client->country_id)
$billto->setCountry($this->authorize->client->country->name);
$billto->setPhoneNumber($this->authorize->client->phone);
}
// Create a new Customer Payment Profile object
$paymentprofile = new CustomerPaymentProfileType();
$paymentprofile->setCustomerType('individual');
if($billto)
$paymentprofile->setBillTo($billto);
$paymentprofile->setPayment($paymentOne);
$paymentprofile->setDefaultPaymentProfile(true);
$paymentprofiles[] = $paymentprofile;
// Assemble the complete transaction request
$paymentprofilerequest = new CreateCustomerPaymentProfileRequest();
$paymentprofilerequest->setMerchantAuthentication($this->authorize->merchant_authentication);
// Add an existing profile id to the request
$paymentprofilerequest->setCustomerProfileId($gateway_customer_reference);
$paymentprofilerequest->setPaymentProfile($paymentprofile);
$paymentprofilerequest->setValidationMode("liveMode");
// Create the controller and get the response
$controller = new CreateCustomerPaymentProfileController($paymentprofilerequest);
$response = $controller->executeWithApiResponse($this->authorize->mode());
if (($response != null) && ($response->getMessages()->getResultCode() == "Ok") ) {
return $this->getPaymentProfile($gateway_customer_reference, $response->getCustomerPaymentProfileId());
} else {
$errorMessages = $response->getMessages()->getMessage();
$message = "Unable to add customer to Authorize.net gateway";
if(is_array($errorMessages))
$message = $errorMessages[0]->getCode() . " " .$errorMessages[0]->getText();
throw new GenericPaymentDriverFailure($message);
}
}
public function getPaymentProfile($gateway_customer_reference, $payment_profile_id)
{
error_reporting (E_ALL & ~E_DEPRECATED);
$this->authorize->init();
// Set the transaction's refId
$refId = 'ref' . time();
//request requires customerProfileId and customerPaymentProfileId
$request = new GetCustomerPaymentProfileRequest();
$request->setMerchantAuthentication($this->authorize->merchant_authentication);
$request->setRefId($refId);
$request->setCustomerProfileId($gateway_customer_reference);
$request->setCustomerPaymentProfileId($payment_profile_id);
$controller = new GetCustomerPaymentProfileController($request);
$response = $controller->executeWithApiResponse($this->authorize->mode());
if(($response != null) && ($response->getMessages()->getResultCode() == "Ok")) {
return $response;
}
else if($response){
$errorMessages = $response->getMessages()->getMessage();
$message = "Unable to add payment method to Authorize.net gateway";
if(is_array($errorMessages))
$message = $errorMessages[0]->getCode() . " " .$errorMessages[0]->getText();
throw new GenericPaymentDriverFailure($message);
}
else
throw new GenericPaymentDriverFailure("Error communicating with Authorize.net");
}
}

View File

@ -0,0 +1,66 @@
<?php
/**
* Invoice Ninja (https://invoiceninja.com)
*
* @link https://github.com/invoiceninja/invoiceninja source repository
*
* @copyright Copyright (c) 2020. Invoice Ninja LLC (https://invoiceninja.com)
*
* @license https://opensource.org/licenses/AAL
*/
namespace App\PaymentDrivers\Authorize;
use net\authorize\api\contract\v1\GetTransactionDetailsRequest;
use net\authorize\api\controller\GetTransactionDetailsController;
/**
* Class AuthorizeTransactions
* @package App\PaymentDrivers\Authorize
*
*/
class AuthorizeTransactions
{
public $authorize;
public function __construct(AuthorizePaymentDriver $authorize)
{
$this->authorize = $authorize;
}
function getTransactionDetails($transactionId)
{
/* Create a merchantAuthenticationType object with authentication details
retrieved from the constants file */
$this->authorize->init();
// Set the transaction's refId
$refId = 'ref' . time();
$request = new GetTransactionDetailsRequest();
$request->setMerchantAuthentication($this->authorize->merchant_authentication);
$request->setTransId($transactionId);
$controller = new GetTransactionDetailsController($request);
$response = $controller->executeWithApiResponse($this->authorize->mode());
if (($response != null) && ($response->getMessages()->getResultCode() == "Ok"))
{
echo "SUCCESS: Transaction Status:" . $response->getTransaction()->getTransactionStatus() . "\n";
echo " Auth Amount:" . $response->getTransaction()->getAuthAmount() . "\n";
echo " Trans ID:" . $response->getTransaction()->getTransId() . "\n";
}
else
{
echo "ERROR : Invalid response\n";
$errorMessages = $response->getMessages()->getMessage();
echo "Response : " . $errorMessages[0]->getCode() . " " .$errorMessages[0]->getText() . "\n";
}
return $response;
}
}

View File

@ -0,0 +1,119 @@
<?php
/**
* Invoice Ninja (https://invoiceninja.com)
*
* @link https://github.com/invoiceninja/invoiceninja source repository
*
* @copyright Copyright (c) 2020. Invoice Ninja LLC (https://invoiceninja.com)
*
* @license https://opensource.org/licenses/AAL
*/
namespace App\PaymentDrivers\Authorize;
use App\PaymentDrivers\AuthorizePaymentDriver;
use net\authorize\api\contract\v1\CreateTransactionRequest;
use net\authorize\api\contract\v1\CustomerProfilePaymentType;
use net\authorize\api\contract\v1\PaymentProfileType;
use net\authorize\api\contract\v1\TransactionRequestType;
use net\authorize\api\controller\CreateTransactionController;
/**
* Class ChargePaymentProfile
* @package App\PaymentDrivers\Authorize
*
*/
class ChargePaymentProfile
{
public function __construct(AuthorizePaymentDriver $authorize)
{
$this->authorize = $authorize;
}
function chargeCustomerProfile($profile_id, $payment_profile_id, $amount)
{
$this->authorize->init();
// Set the transaction's refId
$refId = 'ref' . time();
$profileToCharge = new CustomerProfilePaymentType();
$profileToCharge->setCustomerProfileId($profile_id);
$paymentProfile = new PaymentProfileType();
$paymentProfile->setPaymentProfileId($payment_profile_id);
$profileToCharge->setPaymentProfile($paymentProfile);
$transactionRequestType = new TransactionRequestType();
$transactionRequestType->setTransactionType("authCaptureTransaction");
$transactionRequestType->setAmount($amount);
$transactionRequestType->setProfile($profileToCharge);
$transactionRequestType->setCurrencyCode($this->authorize->client->currency()->code);
$request = new CreateTransactionRequest();
$request->setMerchantAuthentication($this->authorize->merchant_authentication);
$request->setRefId( $refId);
$request->setTransactionRequest( $transactionRequestType);
$controller = new CreateTransactionController($request);
$response = $controller->executeWithApiResponse($this->authorize->mode());
if($response != null && $response->getMessages()->getResultCode() == "Ok")
{
$tresponse = $response->getTransactionResponse();
if ($tresponse != null && $tresponse->getMessages() != null)
{
info(" Transaction Response code : " . $tresponse->getResponseCode() );
info( "Charge Customer Profile APPROVED :" );
info(" Charge Customer Profile AUTH CODE : " . $tresponse->getAuthCode() );
info(" Charge Customer Profile TRANS ID : " . $tresponse->getTransId() );
info(" Code : " . $tresponse->getMessages()[0]->getCode());
info(" Description : " . $tresponse->getMessages()[0]->getDescription());
//info(" Charge Customer Profile TRANS STATUS : " . $tresponse->getTransactionStatus() );
//info(" Charge Customer Profile Amount : " . $tresponse->getAuthAmount());
info(" Code : " . $tresponse->getMessages()[0]->getCode() );
info(" Description : " . $tresponse->getMessages()[0]->getDescription() );
info(print_r($tresponse->getMessages()[0],1));
}
else
{
info("Transaction Failed ");
if($tresponse->getErrors() != null)
{
info(" Error code : " . $tresponse->getErrors()[0]->getErrorCode() );
info(" Error message : " . $tresponse->getErrors()[0]->getErrorText() );
info(print_r($tresponse->getErrors()[0],1));
}
}
}
else
{
info("Transaction Failed ");
$tresponse = $response->getTransactionResponse();
if($tresponse != null && $tresponse->getErrors() != null)
{
info(" Error code : " . $tresponse->getErrors()[0]->getErrorCode() );
info(" Error message : " . $tresponse->getErrors()[0]->getErrorText() );
info(print_r($tresponse->getErrors()[0],1));
}
else
{
info(" Error code : " . $response->getMessages()->getMessage()[0]->getCode() );
info(" Error message : " . $response->getMessages()->getMessage()[0]->getText() );
}
}
return [
'response' => $response,
'amount' => $amount,
'profile_id' => $profile_id,
'payment_profile_id' => $payment_profile_id
];
}
}

View File

@ -0,0 +1,113 @@
<?php
/**
* Invoice Ninja (https://invoiceninja.com)
*
* @link https://github.com/invoiceninja/invoiceninja source repository
*
* @copyright Copyright (c) 2020. Invoice Ninja LLC (https://invoiceninja.com)
*
* @license https://opensource.org/licenses/AAL
*/
namespace App\PaymentDrivers\Authorize;
use App\PaymentDrivers\AuthorizePaymentDriver;
use net\authorize\api\contract\v1\CreateTransactionRequest;
use net\authorize\api\contract\v1\CustomerProfilePaymentType;
use net\authorize\api\contract\v1\PaymentProfileType;
use net\authorize\api\contract\v1\TransactionRequestType;
use net\authorize\api\controller\CreateTransactionController;
/**
* Class RefundTransaction
* @package App\PaymentDrivers\Authorize
*
*/
class RefundTransaction
{
public function __construct(AuthorizePaymentDriver $authorize)
{
$this->authorize = $authorize;
}
function refundTransaction($transaction_reference, $amount, $payment_profile_id, $profile_id)
{
$this->authorize->init();
// Set the transaction's refId
$refId = 'ref' . time();
$paymentProfile = new PaymentProfileType();
$paymentProfile->setPaymentProfileId( $payment_profile_id );
// set customer profile
$customerProfile = new CustomerProfilePaymentType();
$customerProfile->setCustomerProfileId( $profile_id );
$customerProfile->setPaymentProfile( $paymentProfile );
//create a transaction
$transactionRequest = new TransactionRequestType();
$transactionRequest->setTransactionType("refundTransaction");
$transactionRequest->setAmount($amount);
$transactionRequest->setProfile($customerProfile);
$transactionRequest->setRefTransId($transaction_reference);
$request = new CreateTransactionRequest();
$request->setMerchantAuthentication($this->authorize->merchant_authentication);
$request->setRefId($refId);
$request->setTransactionRequest($transactionRequest);
$controller = new CreateTransactionController($request);
$response = $controller->executeWithApiResponse($this->authorize->mode());
if ($response != null)
{
if($response->getMessages()->getResultCode() == "Ok")
{
$tresponse = $response->getTransactionResponse();
if ($tresponse != null && $tresponse->getMessages() != null)
{
echo " Transaction Response code : " . $tresponse->getResponseCode() . "\n";
echo "Refund SUCCESS: " . $tresponse->getTransId() . "\n";
echo " Code : " . $tresponse->getMessages()[0]->getCode() . "\n";
echo " Description : " . $tresponse->getMessages()[0]->getDescription() . "\n";
}
else
{
echo "Transaction Failed \n";
if($tresponse->getErrors() != null)
{
echo " Error code : " . $tresponse->getErrors()[0]->getErrorCode() . "\n";
echo " Error message : " . $tresponse->getErrors()[0]->getErrorText() . "\n";
}
}
}
else
{
echo "Transaction Failed \n";
$tresponse = $response->getTransactionResponse();
if($tresponse != null && $tresponse->getErrors() != null)
{
echo " Error code : " . $tresponse->getErrors()[0]->getErrorCode() . "\n";
echo " Error message : " . $tresponse->getErrors()[0]->getErrorText() . "\n";
}
else
{
echo " Error code : " . $response->getMessages()->getMessage()[0]->getCode() . "\n";
echo " Error message : " . $response->getMessages()->getMessage()[0]->getText() . "\n";
}
}
}
else
{
echo "No response returned \n";
}
return $response;
}
}

View File

@ -0,0 +1,141 @@
<?php
/**
* Invoice Ninja (https://invoiceninja.com)
*
* @link https://github.com/invoiceninja/invoiceninja source repository
*
* @copyright Copyright (c) 2020. Invoice Ninja LLC (https://invoiceninja.com)
*
* @license https://opensource.org/licenses/AAL
*/
namespace App\PaymentDrivers;
use App\Models\ClientGatewayToken;
use App\Models\GatewayType;
use App\PaymentDrivers\Authorize\AuthorizeCreditCard;
use App\PaymentDrivers\Authorize\AuthorizePaymentMethod;
use net\authorize\api\constants\ANetEnvironment;
use net\authorize\api\contract\v1\CreateTransactionRequest;
use net\authorize\api\contract\v1\GetMerchantDetailsRequest;
use net\authorize\api\contract\v1\MerchantAuthenticationType;
use net\authorize\api\controller\CreateTransactionController;
use net\authorize\api\controller\GetMerchantDetailsController;
/**
* Class BaseDriver
* @package App\PaymentDrivers
*
*/
class AuthorizePaymentDriver extends BaseDriver
{
public $merchant_authentication;
public static $methods = [
GatewayType::CREDIT_CARD => AuthorizeCreditCard::class,
];
public function setPaymentMethod($payment_method_id)
{
$class = self::$methods[$payment_method_id];
$this->payment_method = new $class($this);
return $this;
}
/**
* Returns the gateway types
*/
public function gatewayTypes() :array
{
$types = [
GatewayType::CREDIT_CARD,
];
return $types;
}
public function init()
{
error_reporting (E_ALL & ~E_DEPRECATED);
$this->merchant_authentication = new MerchantAuthenticationType();
$this->merchant_authentication->setName($this->company_gateway->getConfigField('apiLoginId'));
$this->merchant_authentication->setTransactionKey($this->company_gateway->getConfigField('transactionKey'));
return $this;
}
public function getPublicClientKey()
{
$request = new GetMerchantDetailsRequest();
$request->setMerchantAuthentication($this->merchant_authentication);
$controller = new GetMerchantDetailsController($request);
$response = $controller->executeWithApiResponse($this->mode());
return $response->getPublicClientKey();
}
public function mode()
{
if($this->company_gateway->getConfigField('testMode'))
return ANetEnvironment::SANDBOX;
return $env = ANetEnvironment::PRODUCTION;
}
public function authorizeView($payment_method)
{
return (new AuthorizePaymentMethod($this))->authorizeView($payment_method);
}
public function authorizeResponseView(array $data)
{
return (new AuthorizePaymentMethod($this))->authorizeResponseView($data['gateway_type_id'], $data);
}
public function authorize($payment_method)
{
return $this->authorizeView($payment_method);
}
public function processPaymentView($data)
{
return $this->payment_method->processPaymentView($data);
}
public function processPaymentResponse($request)
{
return $this->payment_method->processPaymentResponse($request);
}
public function purchase($amount, $return_client_response = false)
{
return false;
}
public function refund($amount, $transaction_reference, $return_client_response = false)
{
}
public function findClientGatewayRecord() :?ClientGatewayToken
{
return ClientGatewayToken::where('client_id', $this->client->id)
->where('company_gateway_id', $this->company_gateway->id)
->first();
}
}

View File

@ -0,0 +1,120 @@
<?php
/**
* Invoice Ninja (https://invoiceninja.com)
*
* @link https://github.com/invoiceninja/invoiceninja source repository
*
* @copyright Copyright (c) 2020. Invoice Ninja LLC (https://invoiceninja.com)
*
* @license https://opensource.org/licenses/AAL
*/
namespace App\PaymentDrivers;
use App\Models\Client;
use App\Models\CompanyGateway;
use App\Models\Invoice;
use App\Models\Payment;
use App\PaymentDrivers\AbstractPaymentDriver;
use App\Utils\Traits\MakesHash;
use App\Utils\Traits\SystemLogTrait;
/**
* Class BaseDriver
* @package App\PaymentDrivers
*
*/
class BaseDriver extends AbstractPaymentDriver
{
use SystemLogTrait;
use MakesHash;
/* The company gateway instance*/
public $company_gateway;
/* The Invitation */
public $invitation;
/* Gateway capabilities */
public $refundable = false;
/* Token billing */
public $token_billing = false;
/* Authorise payment methods */
public $can_authorise_credit_card = false;
/* The client */
public $client;
public $payment_method;
public static $methods = [];
public function __construct(CompanyGateway $company_gateway, Client $client = null, $invitation = false)
{
$this->company_gateway = $company_gateway;
$this->invitation = $invitation;
$this->client = $client;
}
/**
* Authorize a payment method.
*
* Returns a reusable token for storage for future payments
* @param const $payment_method the GatewayType::constant
* @return view Return a view for collecting payment method information
*/
public function authorize($payment_method) {}
/**
* Executes purchase attempt for a given amount
*
* @param float $amount The amount to be collected
* @param boolean $return_client_response Whether the method needs to return a response (otherwise we assume an unattended payment)
* @return mixed
*/
public function purchase($amount, $return_client_response = false) {}
/**
* Executes a refund attempt for a given amount with a transaction_reference
*
* @param float $amount The amount to be refunded
* @param string $transaction_reference The transaction reference
* @param boolean $return_client_response Whether the method needs to return a response (otherwise we assume an unattended payment)
* @return mixed
*/
public function refund($amount, $transaction_reference, $return_client_response = false) {}
/**
* Set the inbound request payment method type for access.
*
* @param int $payment_method_id The Payment Method ID
*/
public function setPaymentMethod($payment_method_id){}
/**
* Helper method to attach invoices to a payment
*
* @param Payment $payment The payment
* @param array $hashed_ids The array of invoice hashed_ids
* @return Payment The payment object
*/
public function attachInvoices(Payment $payment, $hashed_ids): Payment
{
$transformed = $this->transformKeys($hashed_ids);
$array = is_array($transformed) ? $transformed : [$transformed];
$invoices = Invoice::whereIn('id', $array)
->whereClientId($this->client->id)
->get();
$payment->invoices()->sync($invoices);
$payment->save();
return $payment;
}
}

View File

@ -48,7 +48,7 @@ class BasePaymentDriver
use MakesHash; use MakesHash;
/* The company gateway instance*/ /* The company gateway instance*/
protected $company_gateway; public $company_gateway;
/* The Omnipay payment driver instance*/ /* The Omnipay payment driver instance*/
protected $gateway; protected $gateway;
@ -259,12 +259,12 @@ class BasePaymentDriver
->send(); ->send();
} }
public function createPayment($data): Payment public function createPayment($data, $status = Payment::STATUS_COMPLETED): Payment
{ {
$payment = PaymentFactory::create($this->client->company->id, $this->client->user->id); $payment = PaymentFactory::create($this->client->company->id, $this->client->user->id);
$payment->client_id = $this->client->id; $payment->client_id = $this->client->id;
$payment->company_gateway_id = $this->company_gateway->id; $payment->company_gateway_id = $this->company_gateway->id;
$payment->status_id = Payment::STATUS_COMPLETED; $payment->status_id = $status;
$payment->currency_id = $this->client->getSetting('currency_id'); $payment->currency_id = $this->client->getSetting('currency_id');
$payment->date = Carbon::now(); $payment->date = Carbon::now();

View File

@ -0,0 +1,42 @@
<?php
/**
* Invoice Ninja (https://invoiceninja.com)
*
* @link https://github.com/invoiceninja/invoiceninja source repository
*
* @copyright Copyright (c) 2020. Invoice Ninja LLC (https://invoiceninja.com)
*
* @license https://opensource.org/licenses/AAL
*/
namespace App\PaymentDrivers\CheckoutCom;
trait Utilities
{
public function getPublishableKey()
{
return $this->company_gateway->getConfigField('publicApiKey');
}
public function convertToCheckoutAmount($amount, $currency)
{
$cases = [
'option_1' => ['BIF', 'DJF', 'GNF', 'ISK', 'KMF', 'XAF', 'CLF', 'XPF', 'JPY', 'PYG', 'RWF', 'KRW', 'VUV', 'VND', 'XOF'],
'option_2' => ['BHD', 'IQD', 'JOD', 'KWD', 'LYD', 'OMR', 'TND'],
];
// https://docs.checkout.com/resources/calculating-the-value#Calculatingthevalue-Option1:Thefullvaluefullvalue
if (in_array($currency, $cases['option_1'])) {
return round($amount);
}
// https://docs.checkout.com/resources/calculating-the-value#Calculatingthevalue-Option2:Thevaluedividedby1000valuediv1000
if (in_array($currency, $cases['option_2'])) {
return round($amount * 1000);
}
// https://docs.checkout.com/resources/calculating-the-value#Calculatingthevalue-Option3:Thevaluedividedby100valuediv100
return round($amount * 100);
}
}

View File

@ -0,0 +1,276 @@
<?php
/**
* Invoice Ninja (https://invoiceninja.com)
*
* @link https://github.com/invoiceninja/invoiceninja source repository
*
* @copyright Copyright (c) 2020. Invoice Ninja LLC (https://invoiceninja.com)
*
* @license https://opensource.org/licenses/AAL
*/
namespace App\PaymentDrivers;
use App\Events\Payment\PaymentWasCreated;
use App\Jobs\Mail\PaymentFailureMailer;
use App\Jobs\Util\SystemLogger;
use App\Models\ClientGatewayToken;
use App\Models\GatewayType;
use App\Models\Payment;
use App\Models\PaymentType;
use App\Models\SystemLog;
use App\PaymentDrivers\CheckoutCom\Utilities;
use App\Utils\Traits\SystemLogTrait;
use Checkout\CheckoutApi;
use Checkout\Library\Exceptions\CheckoutHttpException;
use Checkout\Models\Payments\IdSource;
use Checkout\Models\Payments\Payment as CheckoutPayment;
use Checkout\Models\Payments\TokenSource;
class CheckoutComPaymentDriver extends BasePaymentDriver
{
use SystemLogTrait, Utilities;
/* The company gateway instance*/
public $company_gateway;
/* The Invitation */
protected $invitation;
/* Gateway capabilities */
protected $refundable = true;
/* Token billing */
protected $token_billing = true;
/* Authorise payment methods */
protected $can_authorise_credit_card = true;
/** Instance of \Checkout\CheckoutApi */
public $gateway;
/** Since with Checkout.com we handle only credit cards, this method should be empty. */
public function setPaymentMethod($string = null)
{
return $this;
}
public function init()
{
$config = [
'secret' => $this->company_gateway->getConfigField('secretApiKey'),
'public' => $this->company_gateway->getConfigField('publicApiKey'),
'sandbox' => $this->company_gateway->getConfigField('testMode'),
];
$this->gateway = new CheckoutApi($config['secret'], $config['sandbox'], $config['public']);
}
public function viewForType($gateway_type_id)
{
if ($gateway_type_id == GatewayType::CREDIT_CARD) {
return 'gateways.checkout.credit_card';
}
if ($gateway_type_id == GatewayType::TOKEN) {
return 'gateways.checkout.credit_card';
}
}
public function processPaymentView(array $data)
{
$data['gateway'] = $this;
$data['client'] = $this->client;
$data['currency'] = $this->client->getCurrencyCode();
$data['value'] = $this->convertToCheckoutAmount($data['amount_with_fee'], $this->client->getCurrencyCode());
$data['raw_value'] = $data['amount_with_fee'];
$data['customer_email'] = $this->client->present()->email;
return render($this->viewForType($data['payment_method_id']), $data);
}
public function processPaymentResponse($request)
{
$this->init();
$state = [
'server_response' => json_decode($request->gateway_response),
'value' => $request->value,
'raw_value' => $request->raw_value,
'currency' => $request->currency,
];
$state = array_merge($state, $request->all());
$state['store_card'] = boolval($state['store_card']);
if ($request->has('token') && !is_null($request->token)) {
$method = new IdSource($state['token']);
$payment = new CheckoutPayment($method, $state['currency']);
$payment->capture = false;
$payment->amount = $state['value'];
} else {
$method = new TokenSource($state['server_response']->cardToken);
$payment = new CheckoutPayment($method, $state['currency']);
$payment->amount = $state['value'];
if ($this->client->currency()->code === 'EUR') {
$payment->{"3ds"} = ['enabled' => true];
}
}
try {
$response = $this->gateway->payments()->request($payment);
$state['payment_response'] = $response;
if ($response->status === 'Authorized') {
return $this->processSuccessfulPayment($state);
}
if ($response->status === 'Pending') {
return $this->processPendingPayment($state);
}
if ($response->status === 'Declined') {
return $this->processUnsuccessfulPayment($state);
}
} catch (CheckoutHttpException $e) {
return $this->processInternallyFailedPayment($e, $state);
}
}
public function processSuccessfulPayment($state)
{
$state['charge_id'] = $state['payment_response']->id;
if (isset($state['store_card']) && $state['store_card']) {
$this->saveCard($state);
}
$data = [
'payment_method' => $state['charge_id'],
'payment_type' => PaymentType::parseCardType($state['payment_response']->source['scheme']),
'amount' => $state['raw_value'],
];
$payment = $this->createPayment($data, Payment::STATUS_COMPLETED);
$this->attachInvoices($payment, $state['hashed_ids']);
$payment->service()->updateInvoicePayment();
event(new PaymentWasCreated($payment, $payment->company));
$logger_message = [
'server_response' => $state['payment_response'],
'data' => $data
];
SystemLogger::dispatch($logger_message, SystemLog::CATEGORY_GATEWAY_RESPONSE, SystemLog::EVENT_GATEWAY_SUCCESS, SystemLog::TYPE_CHECKOUT, $this->client);
return redirect()->route('client.payments.show', ['payment' => $this->encodePrimaryKey($payment->id)]);
}
public function processPendingPayment($state)
{
$state['charge_id'] = $state['payment_response']->id;
if (isset($state['store_card']) && $state['store_card']) {
$this->saveCard($state);
}
$data = [
'payment_method' => $state['charge_id'],
'payment_type' => PaymentType::parseCardType($state['payment_response']->source['scheme']),
'amount' => $state['raw_value'],
];
$payment = $this->createPayment($data, Payment::STATUS_PENDING);
$this->attachInvoices($payment, $state['hashed_ids']);
$payment->service()->updateInvoicePayment();
event(new PaymentWasCreated($payment, $payment->company));
$logger_message = [
'server_response' => $state['payment_response'],
'data' => $data
];
SystemLogger::dispatch($logger_message, SystemLog::CATEGORY_GATEWAY_RESPONSE, SystemLog::EVENT_GATEWAY_SUCCESS, SystemLog::TYPE_CHECKOUT, $this->client);
try {
return redirect($state['payment_response']->_links['redirect']['href']);
} catch (\Exception $e) {
SystemLogger::dispatch($logger_message, SystemLog::CATEGORY_GATEWAY_RESPONSE, SystemLog::EVENT_GATEWAY_FAILURE, SystemLog::TYPE_CHECKOUT, $this->client);
throw new \Exception('Failed to process the payment.', 1);
}
}
public function processUnsuccessfulPayment($state)
{
PaymentFailureMailer::dispatch($this->client, $state['payment_response']->response_summary, $this->client->company, $state['payment_response']->amount);
$message = [
'server_response' => $state['server_response'],
'data' => $state,
];
SystemLogger::dispatch($message, SystemLog::CATEGORY_GATEWAY_RESPONSE, SystemLog::EVENT_GATEWAY_FAILURE, SystemLog::TYPE_CHECKOUT, $this->client);
throw new \Exception('Failed to process the payment: ' . $state['payment_response']->response_summary, 1);
}
public function processInternallyFailedPayment($e, $state)
{
$message = json_decode($e->getBody());
PaymentFailureMailer::dispatch($this->client, $message->error_type, $this->client->company, $state['value']);
$message = [
'server_response' => $state['server_response'],
'data' => $message,
];
SystemLogger::dispatch($message, SystemLog::CATEGORY_GATEWAY_RESPONSE, SystemLog::EVENT_GATEWAY_FAILURE, SystemLog::TYPE_CHECKOUT, $this->client);
throw new \Exception('Failed to process the payment.', 1);
}
public function createPayment($data, $status = Payment::STATUS_COMPLETED): Payment
{
$payment = parent::createPayment($data, $status);
$client_contact = $this->getContact();
$client_contact_id = $client_contact ? $client_contact->id : null;
$payment->amount = $data['amount'];
$payment->type_id = $data['payment_type'];
$payment->transaction_reference = $data['payment_method'];
$payment->client_contact_id = $client_contact_id;
$payment->save();
return $payment;
}
public function saveCard($state)
{
$company_gateway_token = new ClientGatewayToken();
$company_gateway_token->company_id = $this->client->company->id;
$company_gateway_token->client_id = $this->client->id;
$company_gateway_token->token = $state['payment_response']->source['id'];
$company_gateway_token->company_gateway_id = $this->company_gateway->id;
$company_gateway_token->gateway_type_id = $state['payment_method_id'];
$company_gateway_token->meta = $state['payment_response']->source;
$company_gateway_token->save();
if ($this->client->gateway_tokens->count() == 1) {
$this->client->gateway_tokens()->update(['is_default' => 0]);
$company_gateway_token->is_default = 1;
$company_gateway_token->save();
}
}
}

View File

@ -1,139 +0,0 @@
<?php
/**
* Invoice Ninja (https://invoiceninja.com)
*
* @link https://github.com/invoiceninja/invoiceninja source repository
*
* @copyright Copyright (c) 2020. Invoice Ninja LLC (https://invoiceninja.com)
*
* @license https://opensource.org/licenses/AAL
*/
namespace App\PaymentDrivers;
use App\Factory\PaymentFactory;
use App\Models\Client;
use App\Models\ClientContact;
use App\Models\CompanyGateway;
use App\Models\GatewayType;
use App\Models\Invoice;
use App\Models\Payment;
use App\Utils\Traits\SystemLogTrait;
use Illuminate\Support\Carbon;
use Illuminate\Support\Facades\Auth;
use Omnipay\Omnipay;
/**
* Class BasePaymentDriver
* @package App\PaymentDrivers
*
* Minimum dataset required for payment gateways
*
* $data = [
'amount' => $invoice->getRequestedAmount(),
'currency' => $invoice->getCurrencyCode(),
'returnUrl' => $completeUrl,
'cancelUrl' => $this->invitation->getLink(),
'description' => trans('texts.' . $invoice->getEntityType()) . " {$invoice->number}",
'transactionId' => $invoice->number,
'transactionType' => 'Purchase',
'clientIp' => Request::getClientIp(),
];
*/
class CheckoutPaymentDriver extends BasePaymentDriver
{
use SystemLogTrait;
/* The company gateway instance*/
protected $company_gateway;
/* The Omnipay payment driver instance*/
protected $gateway;
/* The Invitation */
protected $invitation;
/* Gateway capabilities */
protected $refundable = true;
/* Token billing */
protected $token_billing = true;
/* Authorise payment methods */
protected $can_authorise_credit_card = true;
public function createTransactionToken($amount)
{
// if ($this->invoice()->getCurrencyCode() == 'BHD') {
// $amount = $this->invoice()->getRequestedAmount() / 10;
// } elseif ($this->invoice()->getCurrencyCode() == 'KWD') {
// $amount = $this->invoice()->getRequestedAmount() * 10;
// } elseif ($this->invoice()->getCurrencyCode() == 'OMR') {
// $amount = $this->invoice()->getRequestedAmount();
// } else
// $amount = $this->invoice()->getRequestedAmount();
$response = $this->gateway()->purchase([
'amount' => $amount,
'currency' => $this->client->getCurrencyCode(),
])->send();
if ($response->isRedirect()) {
$token = $response->getTransactionReference();
session()->flash('transaction_reference', $token);
// On each request, session()->flash() || sesion('', value) || session[name] ||session->flash(key, value)
return $token;
}
return false;
}
public function viewForType($gateway_type_id)
{
switch ($gateway_type_id) {
case GatewayType::CREDIT_CARD:
return 'gateways.checkout.credit_card';
break;
case GatewayType::TOKEN:
break;
default:
break;
}
}
/**
*
* $data = [
'invoices' => $invoices,
'amount' => $amount,
'fee' => $gateway->calcGatewayFee($amount),
'amount_with_fee' => $amount + $gateway->calcGatewayFee($amount),
'token' => auth()->user()->client->gateway_token($gateway->id, $payment_method_id),
'payment_method_id' => $payment_method_id,
'hashed_ids' => explode(",", request()->input('hashed_ids')),
];
*/
public function processPaymentView(array $data)
{
$data['token'] = $this->createTransactionToken($data['amount']);
$data['gateway'] = $this->gateway();
return render($this->viewForType($data['payment_method_id']), $data);
}
public function processPaymentResponse($request)
{
$data['token'] = session('transaction_reference');
$this->completeOffsitePurchase($data);
}
}

View File

@ -258,9 +258,9 @@ class PayPalExpressPaymentDriver extends BasePaymentDriver
return $items; return $items;
} }
public function createPayment($data): Payment public function createPayment($data, $status = Payment::STATUS_COMPLETED): Payment
{ {
$payment = parent::createPayment($data); $payment = parent::createPayment($data, $status);
$client_contact = $this->getContact(); $client_contact = $this->getContact();
$client_contact_id = $client_contact ? $client_contact->id : null; $client_contact_id = $client_contact ? $client_contact->id : null;

View File

@ -0,0 +1,221 @@
<?php
/**
* Invoice Ninja (https://invoiceninja.com)
*
* @link https://github.com/invoiceninja/invoiceninja source repository
*
* @copyright Copyright (c) 2020. Invoice Ninja LLC (https://invoiceninja.com)
*
* @license https://opensource.org/licenses/AAL
*/
namespace App\PaymentDrivers\Stripe;
use App\Events\Payment\PaymentWasCreated;
use App\Jobs\Mail\PaymentFailureMailer;
use App\Jobs\Util\SystemLogger;
use App\Models\ClientGatewayToken;
use App\Models\GatewayType;
use App\Models\Payment;
use App\Models\PaymentType;
use App\Models\SystemLog;
use App\PaymentDrivers\StripePaymentDriver;
use Stripe\Exception\InvalidRequestException;
class ACH
{
/** @var StripePaymentDriver */
public $stripe;
public function __construct(StripePaymentDriver $stripe)
{
$this->stripe = $stripe;
}
public function authorizeView(array $data)
{
return render('gateways.stripe.ach.authorize', array_merge($data));
}
public function authorizeResponse($request)
{
$state = [
'server_response' => json_decode($request->gateway_response),
'gateway_id' => $request->company_gateway_id,
'gateway_type_id' => $request->gateway_type_id,
'is_default' => $request->is_default,
];
$customer = $this->stripe->findOrCreateCustomer();
$this->stripe->init();
$local_stripe = new \Stripe\StripeClient(
$this->stripe->company_gateway->getConfigField('apiKey')
);
try {
$local_stripe->customers->createSource(
$customer->id,
['source' => $state['server_response']->token->id]
);
} catch (InvalidRequestException $e) {
return back()->with('ach_error', $e->getMessage());
}
$payment_meta = $state['server_response']->token->bank_account;
$payment_meta->brand = ctrans('texts.ach');
$payment_meta->type = ctrans('texts.bank_transfer');
$payment_meta->verified_at = null;
$payment_meta->token = $state['server_response']->token->id;
$client_gateway_token = new ClientGatewayToken();
$client_gateway_token->company_id = $this->stripe->client->company->id;
$client_gateway_token->client_id = $this->stripe->client->id;
$client_gateway_token->token = $state['server_response']->token->bank_account->id;
$client_gateway_token->company_gateway_id = $this->stripe->company_gateway->id;
$client_gateway_token->gateway_type_id = $state['gateway_type_id'];
$client_gateway_token->gateway_customer_reference = $customer->id;
$client_gateway_token->meta = $payment_meta;
$client_gateway_token->save();
if ($state['is_default'] == 'true' || $this->stripe->client->gateway_tokens->count() == 1) {
$this->stripe->client->gateway_tokens()->update(['is_default' => 0]);
$client_gateway_token->is_default = 1;
$client_gateway_token->save();
}
return redirect()->route('client.payment_methods.verification', $client_gateway_token->hashed_id);
}
public function verificationView(ClientGatewayToken $token)
{
return render('gateways.stripe.ach.verify', compact('token'));
}
public function processVerification(ClientGatewayToken $token)
{
$this->stripe->init();
$bank_account = \Stripe\Customer::retrieveSource(
request()->customer,
request()->source,
);
try {
$status = $bank_account->verify(['amounts' => request()->transactions]);
$token->meta->verified_at = now();
$token->save();
return redirect()
->route('client.invoices.index')
->with('success', __('texts.payment_method_verified'));
} catch (\Stripe\Exception\CardException $e) {
return back()->with('error', $e->getMessage());
}
}
public function paymentView(array $data)
{
$state = [
'amount' => $data['amount_with_fee'],
'currency' => $this->stripe->client->getCurrencyCode(),
'invoices' => $data['invoices'],
'gateway' => $this->stripe,
'payment_method_id' => GatewayType::BANK_TRANSFER,
'token' => $data['token'],
'customer' => $this->stripe->findOrCreateCustomer(),
];
return render('gateways.stripe.ach.pay', $state);
}
public function paymentResponse($request)
{
$state = [
'payment_method' => $request->payment_method_id,
'gateway_type_id' => $request->company_gateway_id,
'hashed_ids' => $request->hashed_ids,
'amount' => $this->stripe->convertToStripeAmount($request->amount, $this->stripe->client->currency()->precision),
'currency' => $request->currency,
'source' => $request->source,
'customer' => $request->customer,
];
if ($this->stripe->getContact()) {
$state['client_contact'] = $this->stripe->getContact();
} else {
$state['client_contact'] = $state['invoices']->first()->invitations->first()->contact;
}
$this->stripe->init();
try {
$state['charge'] = \Stripe\Charge::create([
'amount' => $state['amount'],
'currency' => $state['currency'],
'customer' => $state['customer'],
'source' => $state['source'],
]);
if ($state['charge']->status === 'pending' && is_null($state['charge']->failure_message)) {
return $this->processPendingPayment($state);
}
return $this->processUnsuccessfulPayment($state);
} catch (\Exception $e) {
if ($e instanceof \Stripe\Exception\CardException) {
return redirect()->route('client.payment_methods.verification', ClientGatewayToken::first()->hashed_id);
}
}
}
public function processPendingPayment($state)
{
$state['charge_id'] = $state['charge']->id;
$this->stripe->init();
$state['payment_type'] = PaymentType::ACH;
$data = [
'payment_method' => $state['charge_id'],
'payment_type' => $state['payment_type'],
'amount' => $state['charge']->amount,
];
$payment = $this->stripe->createPayment($data, Payment::STATUS_PENDING);
$this->stripe->attachInvoices($payment, $state['hashed_ids']);
$payment->service()->updateInvoicePayment();
event(new PaymentWasCreated($payment, $payment->company));
$logger_message = [
'server_response' => $state['charge'],
'data' => $data,
];
SystemLogger::dispatch($logger_message, SystemLog::CATEGORY_GATEWAY_RESPONSE, SystemLog::EVENT_GATEWAY_SUCCESS, SystemLog::TYPE_STRIPE, $this->stripe->client);
return redirect()->route('client.payments.show', ['payment' => $this->stripe->encodePrimaryKey($payment->id)]);
}
public function processUnsuccessfulPayment($state)
{
PaymentFailureMailer::dispatch($this->stripe->client, $state['charge']->failure_message, $this->stripe->client->company, $state['amount']);
$message = [
'server_response' => $state['charge'],
'data' => $state,
];
SystemLogger::dispatch($message, SystemLog::CATEGORY_GATEWAY_RESPONSE, SystemLog::EVENT_GATEWAY_FAILURE, SystemLog::TYPE_STRIPE, $this->stripe->client);
throw new \Exception('Failed to process the payment.', 1);
}
}

View File

@ -0,0 +1,113 @@
<?php
/**
* Invoice Ninja (https://invoiceninja.com)
*
* @link https://github.com/invoiceninja/invoiceninja source repository
*
* @copyright Copyright (c) 2020. Invoice Ninja LLC (https://invoiceninja.com)
*
* @license https://opensource.org/licenses/AAL
*/
namespace App\PaymentDrivers\Stripe;
use App\Events\Payment\PaymentWasCreated;
use App\Jobs\Mail\PaymentFailureMailer;
use App\Jobs\Util\SystemLogger;
use App\Models\GatewayType;
use App\Models\Payment;
use App\Models\PaymentType;
use App\Models\SystemLog;
use App\PaymentDrivers\StripePaymentDriver;
class Alipay
{
/** @var StripePaymentDriver */
public $stripe;
public function __construct(StripePaymentDriver $stripe)
{
$this->stripe = $stripe;
}
public function paymentView(array $data)
{
$data['gateway'] = $this->stripe;
$data['return_url'] = $this->buildReturnUrl($data);
$data['currency'] = $this->stripe->client->getCurrencyCode();
$data['stripe_amount'] = $this->stripe->convertToStripeAmount($data['amount_with_fee'], $this->stripe->client->currency()->precision);
return render('gateways.stripe.alipay.pay', $data);
}
private function buildReturnUrl($data): string
{
return route('client.payments.response', [
'company_gateway_id' => $this->stripe->company_gateway->id,
'gateway_type_id' => GatewayType::SOFORT,
'hashed_ids' => implode(",", $data['hashed_ids']),
'amount' => $data['amount'],
'fee' => $data['fee'],
]);
}
public function paymentResponse($request)
{
$state = array_merge($request->all(), []);
$amount = $state['amount'] + $state['fee'];
$state['amount'] = $this->stripe->convertToStripeAmount($amount, $this->stripe->client->currency()->precision);
if ($request->redirect_status == 'succeeded') {
return $this->processSuccesfulRedirect($state);
}
return $this->processUnsuccesfulRedirect($state);
}
public function processSuccesfulRedirect($state)
{
$state['charge_id'] = $state['source'];
$this->stripe->init();
$state['payment_type'] = PaymentType::ALIPAY;
$data = [
'payment_method' => $state['charge_id'],
'payment_type' => $state['payment_type'],
'amount' => $state['amount'],
];
$payment = $this->stripe->createPayment($data, Payment::STATUS_PENDING);
if (isset($state['hashed_ids'])) {
$this->stripe->attachInvoices($payment, $state['hashed_ids']);
}
event(new PaymentWasCreated($payment, $payment->company));
$logger_message = [
'server_response' => $state,
'data' => $data
];
SystemLogger::dispatch($logger_message, SystemLog::CATEGORY_GATEWAY_RESPONSE, SystemLog::EVENT_GATEWAY_SUCCESS, SystemLog::TYPE_STRIPE, $this->stripe->client);
return redirect()->route('client.payments.show', ['payment' => $this->stripe->encodePrimaryKey($payment->id)]);
}
public function processUnsuccesfulRedirect($state)
{
PaymentFailureMailer::dispatch($this->stripe->client, $state['charge']->failure_message, $this->stripe->client->company, $state['amount']);
$message = [
'server_response' => $state['charge'],
'data' => $state,
];
SystemLogger::dispatch($message, SystemLog::CATEGORY_GATEWAY_RESPONSE, SystemLog::EVENT_GATEWAY_FAILURE, SystemLog::TYPE_STRIPE, $this->stripe->client);
throw new \Exception('Failed to process the payment.', 1);
}
}

View File

@ -0,0 +1,233 @@
<?php
/**
* Invoice Ninja (https://invoiceninja.com)
*
* @link https://github.com/invoiceninja/invoiceninja source repository
*
* @copyright Copyright (c) 2020. Invoice Ninja LLC (https://invoiceninja.com)
*
* @license https://opensource.org/licenses/AAL
*/
namespace App\PaymentDrivers\Stripe;
use App\Events\Payment\PaymentWasCreated;
use App\Jobs\Mail\PaymentFailureMailer;
use App\Jobs\Util\SystemLogger;
use App\Models\ClientGatewayToken;
use App\Models\GatewayType;
use App\Models\Invoice;
use App\Models\Payment;
use App\Models\PaymentType;
use App\Models\SystemLog;
use App\PaymentDrivers\StripePaymentDriver;
use Stripe\PaymentMethod;
class CreditCard
{
public $stripe;
public function __construct(StripePaymentDriver $stripe)
{
$this->stripe = $stripe;
}
public function authorizeView(array $data)
{
$intent['intent'] = $this->stripe->getSetupIntent();
return render('gateways.stripe.add_credit_card', array_merge($data, $intent));
}
public function authorizeResponse($request)
{
$server_response = json_decode($request->input('gateway_response'));
$gateway_id = $request->input('gateway_id');
$gateway_type_id = $request->input('gateway_type_id');
$is_default = $request->input('is_default');
$payment_method = $server_response->payment_method;
$customer = $this->stripe->findOrCreateCustomer();
$this->stripe->init();
$stripe_payment_method = \Stripe\PaymentMethod::retrieve($payment_method);
$stripe_payment_method_obj = $stripe_payment_method->jsonSerialize();
$stripe_payment_method->attach(['customer' => $customer->id]);
$payment_meta = new \stdClass;
$payment_meta->exp_month = $stripe_payment_method_obj['card']['exp_month'];
$payment_meta->exp_year = $stripe_payment_method_obj['card']['exp_year'];
$payment_meta->brand = $stripe_payment_method_obj['card']['brand'];
$payment_meta->last4 = $stripe_payment_method_obj['card']['last4'];
$payment_meta->type = GatewayType::CREDIT_CARD;
$client_gateway_token = new ClientGatewayToken();
$client_gateway_token->company_id = $this->stripe->client->company->id;
$client_gateway_token->client_id = $this->stripe->client->id;
$client_gateway_token->token = $payment_method;
$client_gateway_token->company_gateway_id = $this->stripe->company_gateway->id;
$client_gateway_token->gateway_type_id = $gateway_type_id;
$client_gateway_token->gateway_customer_reference = $customer->id;
$client_gateway_token->meta = $payment_meta;
$client_gateway_token->save();
if ($is_default == 'true' || $this->stripe->client->gateway_tokens->count() == 1) {
$this->stripe->client->gateway_tokens()->update(['is_default' => 0]);
$client_gateway_token->is_default = 1;
$client_gateway_token->save();
}
return redirect()->route('client.payment_methods.index');
}
public function paymentView(array $data)
{
$payment_intent_data = [
'amount' => $this->stripe->convertToStripeAmount($data['amount_with_fee'], $this->stripe->client->currency()->precision),
'currency' => $this->stripe->client->getCurrencyCode(),
'customer' => $this->stripe->findOrCreateCustomer(),
'description' => $data['invoices']->pluck('id'), //todo more meaningful description here:
];
if ($data['token']) {
$payment_intent_data['payment_method'] = $data['token']->token;
} else {
$payment_intent_data['setup_future_usage'] = 'off_session';
// $payment_intent_data['save_payment_method'] = true;
// $payment_intent_data['confirm'] = true;
}
$data['intent'] = $this->stripe->createPaymentIntent($payment_intent_data);
$data['gateway'] = $this->stripe;
return render('gateways.stripe.credit_card', $data);
}
public function paymentResponse($request)
{
$server_response = json_decode($request->input('gateway_response'));
$state = [
'payment_method' => $server_response->payment_method,
'payment_status' => $server_response->status,
'save_card' => $request->store_card,
'gateway_type_id' => $request->payment_method_id,
'hashed_ids' => $request->hashed_ids,
'server_response' => $server_response,
];
$invoices = Invoice::whereIn('id', $this->stripe->transformKeys($state['hashed_ids']))
->whereClientId($this->stripe->client->id)
->get();
if ($this->stripe->getContact()) {
$client_contact = $this->stripe->getContact();
} else {
$client_contact = $invoices->first()->invitations->first()->contact;
}
$this->stripe->init();
$state['payment_intent'] = \Stripe\PaymentIntent::retrieve($server_response->id);
$state['customer'] = $state['payment_intent']->customer;
if ($state['payment_status'] == 'succeeded') {
return $this->processSuccessfulPayment($state);
}
return $this->processUnsuccessfulPayment($server_response);
}
private function processSuccessfulPayment($state)
{
$state['charge_id'] = $state['payment_intent']->charges->data[0]->id;
$this->stripe->init();
$state['payment_method'] = PaymentMethod::retrieve($state['payment_method']);
$payment_method_object = $state['payment_method']->jsonSerialize();
$state['payment_meta'] = [
'exp_month' => $payment_method_object['card']['exp_month'],
'exp_year' => $payment_method_object['card']['exp_year'],
'brand' => $payment_method_object['card']['brand'],
'last4' => $payment_method_object['card']['last4'],
'type' => $payment_method_object['type'],
];
$payment_type = PaymentType::parseCardType($payment_method_object['card']['brand']);
if ($state['save_card'] === true) {
$this->saveCard($state);
}
// Todo: Need to fix this to support payment types other than credit card.... sepa etc etc
if (!isset($state['payment_type'])) {
$state['payment_type'] = PaymentType::CREDIT_CARD_OTHER;
}
$data = [
'payment_method' => $state['charge_id'],
'payment_type' => $state['payment_type'],
'amount' => $state['server_response']->amount,
];
$payment = $this->stripe->createPayment($data, $status = Payment::STATUS_COMPLETED);
$this->stripe->attachInvoices($payment, $state['hashed_ids']);
$payment->service()->updateInvoicePayment();
event(new PaymentWasCreated($payment, $payment->company));
$logger_message = [
'server_response' => $state['payment_intent'],
'data' => $data
];
SystemLogger::dispatch($logger_message, SystemLog::CATEGORY_GATEWAY_RESPONSE, SystemLog::EVENT_GATEWAY_SUCCESS, SystemLog::TYPE_STRIPE, $this->stripe->client);
return redirect()->route('client.payments.show', ['payment' => $this->stripe->encodePrimaryKey($payment->id)]);
}
private function processUnsuccessfulPayment($server_response)
{
PaymentFailureMailer::dispatch($this->stripe->client, $server_response->cancellation_reason, $this->stripe->client->company, $server_response->amount);
$message = [
'server_response' => $server_response,
'data' => [],
];
SystemLogger::dispatch($message, SystemLog::CATEGORY_GATEWAY_RESPONSE, SystemLog::EVENT_GATEWAY_FAILURE, SystemLog::TYPE_STRIPE, $this->stripe->client);
throw new \Exception('Failed to process the payment.', 1);
}
private function saveCard($state)
{
$state['payment_method']->attach(['customer' => $state['customer']]);
$company_gateway_token = new ClientGatewayToken();
$company_gateway_token->company_id = $this->stripe->client->company->id;
$company_gateway_token->client_id = $this->stripe->client->id;
$company_gateway_token->token = $state['payment_method'];
$company_gateway_token->company_gateway_id = $this->stripe->company_gateway->id;
$company_gateway_token->gateway_type_id = $state['gateway_type_id'];
$company_gateway_token->gateway_customer_reference = $state['customer'];
$company_gateway_token->meta = $state['payment_meta'];
$company_gateway_token->save();
if ($this->stripe->client->gateway_tokens->count() == 1) {
$this->stripe->client->gateway_tokens()->update(['is_default' => 0]);
$company_gateway_token->is_default = 1;
$company_gateway_token->save();
}
}
}

View File

@ -0,0 +1,105 @@
<?php
/**
* Invoice Ninja (https://invoiceninja.com)
*
* @link https://github.com/invoiceninja/invoiceninja source repository
*
* @copyright Copyright (c) 2020. Invoice Ninja LLC (https://invoiceninja.com)
*
* @license https://opensource.org/licenses/AAL
*/
namespace App\PaymentDrivers\Stripe;
use App\Events\Payment\PaymentWasCreated;
use App\Jobs\Util\SystemLogger;
use App\Models\GatewayType;
use App\Models\Payment;
use App\Models\PaymentType;
use App\Models\SystemLog;
use App\PaymentDrivers\StripePaymentDriver;
class SOFORT
{
/** @var StripePaymentDriver */
public $stripe;
public function __construct(StripePaymentDriver $stripe)
{
$this->stripe = $stripe;
}
public function paymentView(array $data)
{
$data['gateway'] = $this->stripe;
$data['return_url'] = $this->buildReturnUrl($data);
$data['stripe_amount'] = $this->stripe->convertToStripeAmount($data['amount_with_fee'], $this->stripe->client->currency()->precision);
$data['client'] = $this->stripe->client;
$data['country'] = $this->stripe->client->country->iso_3166_2;
return render('gateways.stripe.sofort.pay', $data);
}
private function buildReturnUrl($data): string
{
return route('client.payments.response', [
'company_gateway_id' => $this->stripe->company_gateway->id,
'gateway_type_id' => GatewayType::SOFORT,
'hashed_ids' => implode(",", $data['hashed_ids']),
'amount' => $data['amount'],
'fee' => $data['fee'],
]);
}
public function paymentResponse($request)
{
$state = array_merge($request->all(), []);
$amount = $state['amount'] + $state['fee'];
$state['amount'] = $this->stripe->convertToStripeAmount($amount, $this->stripe->client->currency()->precision);
if ($request->redirect_status == 'succeeded') {
return $this->processSuccessfulPayment($state);
}
return $this->processUnsuccessfulPayment($state);
}
public function processSuccessfulPayment($state)
{
$state['charge_id'] = $state['source'];
$this->stripe->init();
$state['payment_type'] = PaymentType::SOFORT;
$data = [
'payment_method' => $state['charge_id'],
'payment_type' => $state['payment_type'],
'amount' => $state['amount'],
];
$payment = $this->stripe->createPayment($data, Payment::STATUS_PENDING);
/** @todo: https://github.com/invoiceninja/invoiceninja/pull/3789/files#r436175798 */
if (isset($state['hashed_ids'])) {
$this->stripe->attachInvoices($payment, $state['hashed_ids']);
}
event(new PaymentWasCreated($payment, $payment->company));
$logger_message = [
'server_response' => $state,
'data' => $data
];
SystemLogger::dispatch($logger_message, SystemLog::CATEGORY_GATEWAY_RESPONSE, SystemLog::EVENT_GATEWAY_SUCCESS, SystemLog::TYPE_STRIPE, $this->stripe->client);
return redirect()->route('client.payments.show', ['payment' => $this->stripe->encodePrimaryKey($payment->id)]);
}
public function processUnsuccessfulPayment($state)
{
return redirect()->route('client.invoices.index')->with('warning', ctrans('texts.status_voided'));
}
}

View File

@ -0,0 +1,26 @@
<?php
/**
* Invoice Ninja (https://invoiceninja.com)
*
* @link https://github.com/invoiceninja/invoiceninja source repository
*
* @copyright Copyright (c) 2020. Invoice Ninja LLC (https://invoiceninja.com)
*
* @license https://opensource.org/licenses/AAL
*/
namespace App\PaymentDrivers\Stripe;
trait Utilities
{
public function convertFromStripeAmount($amount, $precision)
{
return $amount / pow(10, $precision);
}
public function convertToStripeAmount($amount, $precision)
{
return $amount * pow(10, $precision);
}
}

View File

@ -21,6 +21,7 @@ use App\Models\Invoice;
use App\Models\Payment; use App\Models\Payment;
use App\Models\PaymentType; use App\Models\PaymentType;
use App\Models\SystemLog; use App\Models\SystemLog;
use App\PaymentDrivers\Stripe\Utilities;
use App\Utils\Traits\MakesHash; use App\Utils\Traits\MakesHash;
use Illuminate\Http\Request; use Illuminate\Http\Request;
use Illuminate\Support\Carbon; use Illuminate\Support\Carbon;
@ -30,7 +31,7 @@ use Stripe\Stripe;
class StripePaymentDriver extends BasePaymentDriver class StripePaymentDriver extends BasePaymentDriver
{ {
use MakesHash; use MakesHash, Utilities;
protected $refundable = true; protected $refundable = true;
@ -40,6 +41,8 @@ class StripePaymentDriver extends BasePaymentDriver
protected $customer_reference = 'customerReferenceParam'; protected $customer_reference = 'customerReferenceParam';
protected $payment_method;
/** /**
* Methods in this class are divided into * Methods in this class are divided into
* two separate streams * two separate streams
@ -62,6 +65,15 @@ class StripePaymentDriver extends BasePaymentDriver
Stripe::setApiKey($this->company_gateway->getConfigField('apiKey')); Stripe::setApiKey($this->company_gateway->getConfigField('apiKey'));
} }
public function setPaymentMethod(string $method)
{
// Example: setPaymentMethod('App\\PaymentDrivers\\Stripe\\CreditCard');
$this->payment_method = new $method($this);
return $this;
}
/** /**
* Returns the gateway types * Returns the gateway types
*/ */
@ -128,102 +140,39 @@ class StripePaymentDriver extends BasePaymentDriver
} }
/** /**
* Authorises a credit card for future use. * Proxy method to pass the data into payment method authorizeView().
* *
* @param array $data Array of variables needed for the view * @param array $data
*
* @return \Illuminate\Contracts\View\Factory|\Illuminate\View\View * @return \Illuminate\Contracts\View\Factory|\Illuminate\View\View
*/ */
public function authorizeCreditCardView(array $data) public function authorizeView(array $data)
{ {
$intent['intent'] = $this->getSetupIntent(); return $this->payment_method->authorizeView($data);
return render('gateways.stripe.add_credit_card', array_merge($data, $intent));
} }
/** /**
* Processes the gateway response for credit card authorization. * Processes the gateway response for credit card authorization.
* *
* @param Request $request The returning request object * @param \Illuminate\Http\Request $request The returning request object
* @return view Returns the user to payment methods screen.
* @throws \Stripe\Exception\ApiErrorException * @return \Illuminate\Contracts\View\Factory|\Illuminate\View\View
*/ */
public function authorizeCreditCardResponse($request) public function authorizeCreditCardResponse($request)
{ {
$server_response = json_decode($request->input('gateway_response')); return $this->payment_method->authorizeResponse($request);
$gateway_id = $request->input('gateway_id');
$gateway_type_id = $request->input('gateway_type_id');
$is_default = $request->input('is_default');
$payment_method = $server_response->payment_method;
$customer = $this->findOrCreateCustomer();
$this->init();
$stripe_payment_method = \Stripe\PaymentMethod::retrieve($payment_method);
$stripe_payment_method_obj = $stripe_payment_method->jsonSerialize();
$stripe_payment_method->attach(['customer' => $customer->id]);
$payment_meta = new \stdClass;
if ($stripe_payment_method_obj['type'] == 'card') {
$payment_meta->exp_month = $stripe_payment_method_obj['card']['exp_month'];
$payment_meta->exp_year = $stripe_payment_method_obj['card']['exp_year'];
$payment_meta->brand = $stripe_payment_method_obj['card']['brand'];
$payment_meta->last4 = $stripe_payment_method_obj['card']['last4'];
$payment_meta->type = GatewayType::CREDIT_CARD;
}
$cgt = new ClientGatewayToken;
$cgt->company_id = $this->client->company->id;
$cgt->client_id = $this->client->id;
$cgt->token = $payment_method;
$cgt->company_gateway_id = $this->company_gateway->id;
$cgt->gateway_type_id = $gateway_type_id;
$cgt->gateway_customer_reference = $customer->id;
$cgt->meta = $payment_meta;
$cgt->save();
if ($is_default == 'true' || $this->client->gateway_tokens->count() == 1) {
$this->client->gateway_tokens()->update(['is_default'=>0]);
$cgt->is_default = 1;
$cgt->save();
}
return redirect()->route('client.payment_methods.index');
} }
/** /**
* Process the payment with gateway. * Process the payment with gateway.
* *
* @param array $data * @param array $data
* @return \Illuminate\Contracts\View\Factory|\Illuminate\View\View|void * @return \Illuminate\Contracts\View\Factory|\Illuminate\View\View|void
* @throws \Exception
*/ */
public function processPaymentView(array $data) public function processPaymentView(array $data)
{ {
$payment_intent_data = [ return $this->payment_method->paymentView($data);
'amount' => $this->convertToStripeAmount($data['amount_with_fee'], $this->client->currency()->precision),
'currency' => $this->client->getCurrencyCode(),
'customer' => $this->findOrCreateCustomer(),
'description' => $data['invoices']->pluck('id'), //todo more meaningful description here:
];
if ($data['token']) {
$payment_intent_data['payment_method'] = $data['token']->token;
} else {
$payment_intent_data['setup_future_usage'] = 'off_session';
// $payment_intent_data['save_payment_method'] = true;
// $payment_intent_data['confirm'] = true;
}
$data['intent'] = $this->createPaymentIntent($payment_intent_data);
$data['gateway'] = $this;
return render($this->viewForType($data['payment_method_id']), $data);
} }
/** /**
@ -256,137 +205,12 @@ class StripePaymentDriver extends BasePaymentDriver
*/ */
public function processPaymentResponse($request) //We never have to worry about unsuccessful payments as failures are handled at the front end for this driver. public function processPaymentResponse($request) //We never have to worry about unsuccessful payments as failures are handled at the front end for this driver.
{ {
$server_response = json_decode($request->input('gateway_response')); return $this->payment_method->paymentResponse($request);
$payment_method = $server_response->payment_method;
$payment_status = $server_response->status;
$save_card = $request->input('store_card');
$gateway_type_id = $request->input('payment_method_id');
$hashed_ids = $request->input('hashed_ids');
$invoices = Invoice::whereIn('id', $this->transformKeys($hashed_ids))
->whereClientId($this->client->id)
->get();
/**
* Potential statuses that can be returned
*
* requires_action
* processing
* canceled
* requires_action
* requires_confirmation
* requires_payment_method
*
*/
if ($this->getContact()) {
$client_contact = $this->getContact();
} else {
$client_contact = $invoices->first()->invitations->first()->contact;
}
$this->init();
$payment_intent = \Stripe\PaymentIntent::retrieve($server_response->id);
$customer = $payment_intent->customer;
if ($payment_status == 'succeeded') {
$charge_id = $payment_intent->charges->data[0]->id;
$this->init();
$stripe_payment_method = \Stripe\PaymentMethod::retrieve($payment_method);
$stripe_payment_method_obj = $stripe_payment_method->jsonSerialize();
$payment_meta = new \stdClass;
if ($stripe_payment_method_obj['type'] == 'card') {
$payment_meta->exp_month = $stripe_payment_method_obj['card']['exp_month'];
$payment_meta->exp_year = $stripe_payment_method_obj['card']['exp_year'];
$payment_meta->brand = $stripe_payment_method_obj['card']['brand'];
$payment_meta->last4 = $stripe_payment_method_obj['card']['last4'];
$payment_meta->type = $stripe_payment_method_obj['type'];
$payment_type = PaymentType::parseCardType($stripe_payment_method_obj['card']['brand']);
}
if ($save_card == 'true') {
$stripe_payment_method->attach(['customer' => $customer]);
$cgt = new ClientGatewayToken;
$cgt->company_id = $this->client->company->id;
$cgt->client_id = $this->client->id;
$cgt->token = $payment_method;
$cgt->company_gateway_id = $this->company_gateway->id;
$cgt->gateway_type_id = $gateway_type_id;
$cgt->gateway_customer_reference = $customer;
$cgt->meta = $payment_meta;
$cgt->save();
if ($this->client->gateway_tokens->count() == 1) {
$this->client->gateway_tokens()->update(['is_default'=>0]);
$cgt->is_default = 1;
$cgt->save();
}
}
//todo need to fix this to support payment types other than credit card.... sepa etc etc
if (!$payment_type) {
$payment_type = PaymentType::CREDIT_CARD_OTHER;
}
$data = [
'payment_method' => $charge_id,
'payment_type' => $payment_type,
'amount' => $server_response->amount,
];
/* Create payment*/
$payment = $this->createPayment($data);
/* Link invoices to payment*/
$this->attachInvoices($payment, $hashed_ids);
$payment->service()->UpdateInvoicePayment();
event(new PaymentWasCreated($payment, $payment->company));
SystemLogger::dispatch(
[
'server_response' => $payment_intent,
'data' => $data
],
SystemLog::CATEGORY_GATEWAY_RESPONSE,
SystemLog::EVENT_GATEWAY_SUCCESS,
SystemLog::TYPE_STRIPE,
$this->client
);
return redirect()->route('client.payments.show', ['payment' => $this->encodePrimaryKey($payment->id)]);
} else {
PaymentFailureMailer::dispatch($this->client, $server_response->cancellation_reason, $this->client->company, $server_response->amount);
/*Fail and log*/
SystemLogger::dispatch(
[
'server_response' => $server_response,
'data' => $data
],
SystemLog::CATEGORY_GATEWAY_RESPONSE,
SystemLog::EVENT_GATEWAY_FAILURE,
SystemLog::TYPE_STRIPE,
$this->client
);
throw new \Exception("Failed to process payment", 1);
}
} }
public function createPayment($data) :Payment public function createPayment($data, $status = Payment::STATUS_COMPLETED) :Payment
{ {
$payment = parent::createPayment($data); $payment = parent::createPayment($data, $status);
$client_contact = $this->getContact(); $client_contact = $this->getContact();
$client_contact_id = $client_contact ? $client_contact->id : null; $client_contact_id = $client_contact ? $client_contact->id : null;
@ -400,15 +224,6 @@ class StripePaymentDriver extends BasePaymentDriver
return $payment; return $payment;
} }
private function convertFromStripeAmount($amount, $precision)
{
return $amount / pow(10, $precision);
}
private function convertToStripeAmount($amount, $precision)
{
return $amount * pow(10, $precision);
}
/** /**
* Creates a new String Payment Intent * Creates a new String Payment Intent
* *
@ -515,5 +330,15 @@ class StripePaymentDriver extends BasePaymentDriver
return false; return false;
} }
public function verificationView(ClientGatewayToken $payment_method)
{
return $this->payment_method->verificationView($payment_method);
}
public function processVerification(ClientGatewayToken $payment_method)
{
return $this->payment_method->processVerification($payment_method);
}
/************************************** Omnipay API methods **********************************************************/ /************************************** Omnipay API methods **********************************************************/
} }

View File

@ -11,6 +11,7 @@
namespace App\Repositories; namespace App\Repositories;
use App\Libraries\MultiDB;
use App\Models\Activity; use App\Models\Activity;
use App\Models\Backup; use App\Models\Backup;
use App\Models\Client; use App\Models\Client;
@ -31,8 +32,11 @@ class ActivityRepository extends BaseRepository
* @param stdClass $fields The fields * @param stdClass $fields The fields
* @param Collection $entity The entity that you wish to have backed up (typically Invoice, Quote etc etc rather than Payment) * @param Collection $entity The entity that you wish to have backed up (typically Invoice, Quote etc etc rather than Payment)
*/ */
public function save($fields, $entity) public function save($fields, $entity, $db = null)
{ {
if($db)
MultiDB::setDB($db);
$activity = new Activity(); $activity = new Activity();
$activity->is_system = app()->runningInConsole(); $activity->is_system = app()->runningInConsole();

View File

@ -11,6 +11,7 @@
namespace App\Repositories; namespace App\Repositories;
use App\Events\Invoice\InvoiceWasUpdated;
use App\Factory\InvoiceInvitationFactory; use App\Factory\InvoiceInvitationFactory;
use App\Factory\QuoteInvitationFactory; use App\Factory\QuoteInvitationFactory;
use App\Jobs\Product\UpdateOrCreateProduct; use App\Jobs\Product\UpdateOrCreateProduct;
@ -262,7 +263,7 @@ class BaseRepository
//make sure we are creating an invite for a contact who belongs to the client only! //make sure we are creating an invite for a contact who belongs to the client only!
$contact = ClientContact::find($invitation['client_contact_id']); $contact = ClientContact::find($invitation['client_contact_id']);
if ($model->client_id == $contact->client_id); if ($contact && $model->client_id == $contact->client_id);
{ {
$new_invitation = $invitation_factory_class::create($model->company_id, $model->user_id); $new_invitation = $invitation_factory_class::create($model->company_id, $model->user_id);
$new_invitation->{$lcfirst_resource_id} = $model->id; $new_invitation->{$lcfirst_resource_id} = $model->id;
@ -294,6 +295,9 @@ class BaseRepository
} }
$model = $model->calc()->getInvoice(); $model = $model->calc()->getInvoice();
event(new InvoiceWasUpdated($model, $model->company));
} }
if ($class->name == Credit::class) { if ($class->name == Credit::class) {

View File

@ -46,7 +46,6 @@ class PaymentRepository extends BaseRepository
/** /**
* Saves and updates a payment. //todo refactor to handle refunds and payments. * Saves and updates a payment. //todo refactor to handle refunds and payments.
* *
*
* @param array $data the request object * @param array $data the request object
* @param Payment $payment The Payment object * @param Payment $payment The Payment object
* @return Payment|null Payment $payment * @return Payment|null Payment $payment
@ -57,61 +56,85 @@ class PaymentRepository extends BaseRepository
return $this->applyPayment($data, $payment); return $this->applyPayment($data, $payment);
} }
return $this->refundPayment($data, $payment); return $payment;
} }
/** /**
* Handles a positive payment request * Handles a positive payment request
* @param array $data The data object * @param array $data The data object
* @param Payment $payment The $payment entity * @param Payment $payment The $payment entity
* @return Payment The updated/created payment object * @return Payment The updated/created payment object
*/ */
private function applyPayment(array $data, Payment $payment): ?Payment private function applyPayment(array $data, Payment $payment): ?Payment
{ {
//check currencies here and fill the exchange rate data if necessary //check currencies here and fill the exchange rate data if necessary
if (!$payment->id) { if (!$payment->id) {
$this->processExchangeRates($data, $payment); $this->processExchangeRates($data, $payment);
/*We only update the paid to date ONCE per payment*/
if (array_key_exists('invoices', $data) && is_array($data['invoices']) && count($data['invoices']) > 0) {
if($data['amount'] == '')
$data['amount'] = array_sum(array_column($data['invoices'], 'amount'));
$client = Client::find($data['client_id']);
$client->service()->updatePaidToDate($data['amount'])->save();
}
} }
/*Fill the payment*/
$payment->fill($data); $payment->fill($data);
$payment->status_id = Payment::STATUS_COMPLETED; $payment->status_id = Payment::STATUS_COMPLETED;
$payment->save(); $payment->save();
/*Ensure payment number generated*/
if (!$payment->number || strlen($payment->number) == 0) { if (!$payment->number || strlen($payment->number) == 0) {
$payment->number = $payment->client->getNextPaymentNumber($payment->client); $payment->number = $payment->client->getNextPaymentNumber($payment->client);
} }
$payment->client->service()->updatePaidToDate($payment->amount)->save();
$invoice_totals = 0; $invoice_totals = 0;
$credit_totals = 0; $credit_totals = 0;
if (array_key_exists('invoices', $data) && is_array($data['invoices'])) { /*Iterate through invoices and apply payments*/
if (array_key_exists('invoices', $data) && is_array($data['invoices']) && count($data['invoices']) > 0) {
$invoice_totals = array_sum(array_column($data['invoices'], 'amount')); $invoice_totals = array_sum(array_column($data['invoices'], 'amount'));
$invoices = Invoice::whereIn('id', array_column($data['invoices'], 'invoice_id'))->get(); $invoices = Invoice::whereIn('id', array_column($data['invoices'], 'invoice_id'))->get();
$payment->invoices()->saveMany($invoices); $payment->invoices()->saveMany($invoices);
info("iterating through payment invoices");
foreach ($data['invoices'] as $paid_invoice) { foreach ($data['invoices'] as $paid_invoice) {
$invoice = Invoice::whereId($paid_invoice['invoice_id'])->first();
$invoice = Invoice::whereId($paid_invoice['invoice_id'])->with('client')->first();
info("current client balance = {$invoice->client->balance}");
if ($invoice) { if ($invoice) {
$invoice->service()->applyPayment($payment, $paid_invoice['amount'])->save();
info("apply payment amount {$paid_invoice['amount']}");
$invoice = $invoice->service()->markSent()->applyPayment($payment, $paid_invoice['amount'])->save();
info("after processing invoice the client balance is now {$invoice->client->balance}");
} }
} }
} else { } else {
//payment is made, but not to any invoice, therefore we are applying the payment to the clients credit //payment is made, but not to any invoice, therefore we are applying the payment to the clients paid_to_date only
$payment->client->processUnappliedPayment($payment->amount); $payment->client->service()->updatePaidToDate($payment->amount)->save();
} }
if (array_key_exists('credits', $data) && is_array($data['credits'])) { if (array_key_exists('credits', $data) && is_array($data['credits'])) {
$credit_totals = array_sum(array_column($data['credits'], 'amount')); $credit_totals = array_sum(array_column($data['credits'], 'amount'));
$credits = Credit::whereIn('id', $this->transformKeys(array_column($data['credits'], 'credit_id')))->get(); $credits = Credit::whereIn('id', $this->transformKeys(array_column($data['credits'], 'credit_id')))->get();
$payment->credits()->saveMany($credits); $payment->credits()->saveMany($credits);
foreach ($data['credits'] as $paid_credit) { foreach ($data['credits'] as $paid_credit) {
@ -128,8 +151,9 @@ class PaymentRepository extends BaseRepository
$invoice_totals -= $credit_totals; $invoice_totals -= $credit_totals;
//$payment->amount = $invoice_totals; //creates problems when setting amount like this. //$payment->amount = $invoice_totals; //creates problems when setting amount like this.
if($credit_totals == $payment->amount){
if ($invoice_totals == $payment->amount) { $payment->applied += $credit_totals;
} elseif ($invoice_totals == $payment->amount) {
$payment->applied += $payment->amount; $payment->applied += $payment->amount;
} elseif ($invoice_totals < $payment->amount) { } elseif ($invoice_totals < $payment->amount) {
$payment->applied += $invoice_totals; $payment->applied += $invoice_totals;
@ -140,53 +164,6 @@ class PaymentRepository extends BaseRepository
return $payment->fresh(); return $payment->fresh();
} }
/**
* @deprecated Refundable trait replaces this.
*/
private function refundPayment(array $data, Payment $payment): string
{
// //temp variable to sum the total refund/credit amount
// $invoice_total_adjustment = 0;
// if (array_key_exists('invoices', $data) && is_array($data['invoices'])) {
// foreach ($data['invoices'] as $adjusted_invoice) {
// $invoice = Invoice::whereId($adjusted_invoice['invoice_id'])->first();
// $invoice_total_adjustment += $adjusted_invoice['amount'];
// if (array_key_exists('credits', $adjusted_invoice)) {
// //process and insert credit notes
// foreach ($adjusted_invoice['credits'] as $credit) {
// $credit = $this->credit_repo->save($credit, CreditFactory::create(auth()->user()->id, auth()->user()->id), $invoice);
// }
// } else {
// //todo - generate Credit Note for $amount on $invoice - the assumption here is that it is a FULL refund
// }
// }
// if (array_key_exists('amount', $data) && $data['amount'] != $invoice_total_adjustment)
// return 'Amount must equal the sum of invoice adjustments';
// }
// //adjust applied amount
// $payment->applied += $invoice_total_adjustment;
// //adjust clients paid to date
// $client = $payment->client;
// $client->paid_to_date += $invoice_total_adjustment;
// $payment->save();
// $client->save();
}
/** /**
* If the client is paying in a currency other than * If the client is paying in a currency other than

View File

@ -37,7 +37,14 @@ class ApplyPayment extends AbstractService
->ledger() ->ledger()
->updatePaymentBalance($this->payment_amount*-1); ->updatePaymentBalance($this->payment_amount*-1);
$this->payment->client->service()->updateBalance($this->payment_amount*-1)->save(); info("apply payment method - current client balance = {$this->payment->client->balance}");
info("reducing client balance by payment amount {$this->payment_amount}");
$this->invoice->client->service()->updateBalance($this->payment_amount*-1)->save();
// $this->invoice->client->service()->updateBalance($this->payment_amount*-1)->updatePaidToDate($this->payment_amount)->save();
info("post client balance = {$this->invoice->client->balance}");
/* Update Pivot Record amount */ /* Update Pivot Record amount */
$this->payment->invoices->each(function ($inv) { $this->payment->invoices->each(function ($inv) {
@ -47,6 +54,10 @@ class ApplyPayment extends AbstractService
} }
}); });
$this->invoice->fresh('client');
info("1 end of apply payment method the client balance = {$this->invoice->client->balance}");
if ($this->invoice->hasPartial()) { if ($this->invoice->hasPartial()) {
//is partial and amount is exactly the partial amount //is partial and amount is exactly the partial amount
if ($this->invoice->partial == $this->payment_amount) { if ($this->invoice->partial == $this->payment_amount) {
@ -61,9 +72,11 @@ class ApplyPayment extends AbstractService
} elseif ($this->payment_amount < $this->invoice->balance) { //partial invoice payment made } elseif ($this->payment_amount < $this->invoice->balance) { //partial invoice payment made
$this->invoice->service()->clearPartial()->setStatus(Invoice::STATUS_PARTIAL)->updateBalance($this->payment_amount*-1); $this->invoice->service()->clearPartial()->setStatus(Invoice::STATUS_PARTIAL)->updateBalance($this->payment_amount*-1);
} }
info("2 end of apply payment method the client balnace = {$this->invoice->client->balance}");
$this->invoice->service()->applyNumber()->save(); $this->invoice->service()->applyNumber()->save();
info("3 end of apply payment method the client balnace = {$this->invoice->client->balance}");
return $this->invoice; return $this->invoice;
} }
} }

View File

@ -45,6 +45,9 @@ class HandleCancellation extends AbstractService
} }
$adjustment = $this->invoice->balance*-1; $adjustment = $this->invoice->balance*-1;
$this->backupCancellation($adjustment);
//set invoice balance to 0 //set invoice balance to 0
$this->invoice->ledger()->updateInvoiceBalance($adjustment, "Invoice cancellation"); $this->invoice->ledger()->updateInvoiceBalance($adjustment, "Invoice cancellation");
@ -56,6 +59,58 @@ class HandleCancellation extends AbstractService
event(new InvoiceWasCancelled($this->invoice)); event(new InvoiceWasCancelled($this->invoice));
return $this->invoice; return $this->invoice;
} }
public function reverse()
{
$cancellation = $this->invoice->backup->cancellation;
$adjustment = $cancellation->adjustment*-1;
$this->invoice->ledger()->updateInvoiceBalance($adjustment, "Invoice cancellation REVERSAL");
/* Reverse the invoice status and balance */
$this->invoice->balance += $adjustment;
$this->invoice->status_id = $cancellation->status_id;
$this->invoice->client->service()->updateBalance($adjustment)->save();
/* Pop the cancellation out of the backup*/
$backup = $this->invoice->backup;
unset($backup->cancellation);
$this->invoice->backup = $backup;
$this->invoice->save();
return $this->invoice;
}
/**
* Backup the cancellation in case we ever need to reverse it.
*
* @param float $adjustment The amount the balance has been reduced by to cancel the invoice
* @return void
*/
private function backupCancellation($adjustment)
{
if(!is_object($this->invoice->backup)){
$backup = new \stdClass;
$this->invoice->backup = $backup;
}
$cancellation = new \stdClass;
$cancellation->adjustment = $adjustment;
$cancellation->status_id = $this->invoice->status_id;
$invoice_backup = $this->invoice->backup;
$invoice_backup->cancellation = $cancellation;
$this->invoice->backup = $invoice_backup;
$this->invoice->save();
}
} }

View File

@ -44,12 +44,14 @@ class HandleReversal extends AbstractService
return $this->invoice; return $this->invoice;
} }
if($this->invoice->status_id == Invoice::STATUS_CANCELLED)
$this->invoice = $this->invoice->service()->reverseCancellation()->save();
$balance_remaining = $this->invoice->balance; $balance_remaining = $this->invoice->balance;
$total_paid = $this->invoice->amount - $this->invoice->balance; $total_paid = $this->invoice->amount - $this->invoice->balance;
/*Adjust payment applied and the paymentables to the correct amount */ /*Adjust payment applied and the paymentables to the correct amount */
$paymentables = Paymentable::wherePaymentableType(Invoice::class) $paymentables = Paymentable::wherePaymentableType(Invoice::class)
->wherePaymentableId($this->invoice->id) ->wherePaymentableId($this->invoice->id)
->get(); ->get();
@ -69,7 +71,8 @@ class HandleReversal extends AbstractService
if ($total_paid > 0) { if ($total_paid > 0) {
$credit = CreditFactory::create($this->invoice->company_id, $this->invoice->user_id); $credit = CreditFactory::create($this->invoice->company_id, $this->invoice->user_id);
$credit->client_id = $this->invoice->client_id; $credit->client_id = $this->invoice->client_id;
$credit->invoice_id = $this->invoice->id;
$item = InvoiceItemFactory::create(); $item = InvoiceItemFactory::create();
$item->quantity = 1; $item->quantity = 1;
$item->cost = (float)$total_paid; $item->cost = (float)$total_paid;
@ -88,10 +91,12 @@ class HandleReversal extends AbstractService
$credit->service()->markSent()->save(); $credit->service()->markSent()->save();
} }
/* Set invoice balance to 0 */
$this->invoice->ledger()->updateInvoiceBalance($balance_remaining*-1, $notes)->save();
$this->invoice->balance= 0; /* Set invoice balance to 0 */
if($this->invoice->balance != 0)
$this->invoice->ledger()->updateInvoiceBalance($balance_remaining*-1, $notes)->save();
$this->invoice->balance=0;
/* Set invoice status to reversed... somehow*/ /* Set invoice status to reversed... somehow*/
$this->invoice->service()->setStatus(Invoice::STATUS_REVERSED)->save(); $this->invoice->service()->setStatus(Invoice::STATUS_REVERSED)->save();
@ -109,6 +114,42 @@ class HandleReversal extends AbstractService
return $this->invoice; return $this->invoice;
//create a ledger row for this with the resulting Credit ( also include an explanation in the notes section ) //create a ledger row for this with the resulting Credit ( also include an explanation in the notes section )
} }
// public function run2()
// {
// /* Check again!! */
// if (!$this->invoice->invoiceReversable($this->invoice)) {
// return $this->invoice;
// }
// if($this->invoice->status_id == Invoice::STATUS_CANCELLED)
// $this->invoice = $this->invoice->service()->reverseCancellation()->save();
// //$balance_remaining = $this->invoice->balance;
// //$total_paid = $this->invoice->amount - $this->invoice->balance;
// /*Adjust payment applied and the paymentables to the correct amount */
// $paymentables = Paymentable::wherePaymentableType(Invoice::class)
// ->wherePaymentableId($this->invoice->id)
// ->get();
// $total_paid = 0;
// $paymentables->each(function ($paymentable) use ($total_paid) {
// $reversable_amount = $paymentable->amount - $paymentable->refunded;
// $total_paid -= $reversable_amount;
// $paymentable->amount = $paymentable->refunded;
// $paymentable->save();
// });
// //Unwinding any payments made to this invoice
// }
} }
// The client paid to date amount is reduced by the calculated amount of (invoice balance - invoice amount).

View File

@ -129,6 +129,13 @@ class InvoiceService
return $this; return $this;
} }
public function reverseCancellation()
{
$this->invoice = (new HandleCancellation($this->invoice))->reverse();
return $this;
}
public function markViewed() public function markViewed()
{ {
$this->invoice->last_viewed = Carbon::now()->format('Y-m-d H:i'); $this->invoice->last_viewed = Carbon::now()->format('Y-m-d H:i');

View File

@ -48,10 +48,14 @@ class MarkSent extends AbstractService
->setDueDate() ->setDueDate()
->save(); ->save();
info("marking invoice sent currently client balance = {$this->client->balance}");
$this->client->service()->updateBalance($this->invoice->balance)->save(); $this->client->service()->updateBalance($this->invoice->balance)->save();
info("after marking invoice sent currently client balance = {$this->client->balance}");
$this->invoice->ledger()->updateInvoiceBalance($this->invoice->balance); $this->invoice->ledger()->updateInvoiceBalance($this->invoice->balance);
return $this->invoice; return $this->invoice->fresh();
} }
} }

Some files were not shown because too many files have changed in this diff Show More