mirror of
https://github.com/invoiceninja/invoiceninja.git
synced 2025-07-09 03:14:30 -04:00
Added autolink to emails and fixed checkout.com integration
This commit is contained in:
parent
6e5237b7dd
commit
cbe2b2905e
@ -20,12 +20,9 @@ use App\Models\Product;
|
|||||||
use App\Models\TaxRate;
|
use App\Models\TaxRate;
|
||||||
use App\Models\InvoiceDesign;
|
use App\Models\InvoiceDesign;
|
||||||
use App\Models\Activity;
|
use App\Models\Activity;
|
||||||
use App\Models\Gateway;
|
|
||||||
use App\Ninja\Mailers\ContactMailer as Mailer;
|
use App\Ninja\Mailers\ContactMailer as Mailer;
|
||||||
use App\Ninja\Repositories\InvoiceRepository;
|
use App\Ninja\Repositories\InvoiceRepository;
|
||||||
use App\Ninja\Repositories\ClientRepository;
|
use App\Ninja\Repositories\ClientRepository;
|
||||||
use App\Events\InvoiceInvitationWasViewed;
|
|
||||||
use App\Events\QuoteInvitationWasViewed;
|
|
||||||
use App\Services\InvoiceService;
|
use App\Services\InvoiceService;
|
||||||
use App\Services\RecurringInvoiceService;
|
use App\Services\RecurringInvoiceService;
|
||||||
use App\Http\Requests\SaveInvoiceWithClientRequest;
|
use App\Http\Requests\SaveInvoiceWithClientRequest;
|
||||||
@ -86,119 +83,6 @@ class InvoiceController extends BaseController
|
|||||||
return $this->recurringInvoiceService->getDatatable($accountId, $clientPublicId, ENTITY_RECURRING_INVOICE, $search);
|
return $this->recurringInvoiceService->getDatatable($accountId, $clientPublicId, ENTITY_RECURRING_INVOICE, $search);
|
||||||
}
|
}
|
||||||
|
|
||||||
public function view($invitationKey)
|
|
||||||
{
|
|
||||||
if (!$invitation = $this->invoiceRepo->findInvoiceByInvitation($invitationKey)) {
|
|
||||||
return response()->view('error', [
|
|
||||||
'error' => trans('texts.invoice_not_found'),
|
|
||||||
'hideHeader' => true,
|
|
||||||
]);
|
|
||||||
}
|
|
||||||
|
|
||||||
$invoice = $invitation->invoice;
|
|
||||||
$client = $invoice->client;
|
|
||||||
$account = $invoice->account;
|
|
||||||
|
|
||||||
if (!$account->checkSubdomain(Request::server('HTTP_HOST'))) {
|
|
||||||
return response()->view('error', [
|
|
||||||
'error' => trans('texts.invoice_not_found'),
|
|
||||||
'hideHeader' => true,
|
|
||||||
'clientViewCSS' => $account->clientViewCSS(),
|
|
||||||
'clientFontUrl' => $account->getFontsUrl(),
|
|
||||||
]);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!Input::has('phantomjs') && !Session::has($invitationKey) && (!Auth::check() || Auth::user()->account_id != $invoice->account_id)) {
|
|
||||||
if ($invoice->is_quote) {
|
|
||||||
event(new QuoteInvitationWasViewed($invoice, $invitation));
|
|
||||||
} else {
|
|
||||||
event(new InvoiceInvitationWasViewed($invoice, $invitation));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Session::put($invitationKey, true); // track this invitation has been seen
|
|
||||||
Session::put('invitation_key', $invitationKey); // track current invitation
|
|
||||||
|
|
||||||
$account->loadLocalizationSettings($client);
|
|
||||||
|
|
||||||
$invoice->invoice_date = Utils::fromSqlDate($invoice->invoice_date);
|
|
||||||
$invoice->due_date = Utils::fromSqlDate($invoice->due_date);
|
|
||||||
$invoice->is_pro = $account->isPro();
|
|
||||||
$invoice->invoice_fonts = $account->getFontsData();
|
|
||||||
|
|
||||||
if ($invoice->invoice_design_id == CUSTOM_DESIGN) {
|
|
||||||
$invoice->invoice_design->javascript = $account->custom_design;
|
|
||||||
} else {
|
|
||||||
$invoice->invoice_design->javascript = $invoice->invoice_design->pdfmake;
|
|
||||||
}
|
|
||||||
$contact = $invitation->contact; $contact->setVisible([
|
|
||||||
'first_name',
|
|
||||||
'last_name',
|
|
||||||
'email',
|
|
||||||
'phone',
|
|
||||||
]);
|
|
||||||
|
|
||||||
$paymentTypes = $this->getPaymentTypes($client, $invitation);
|
|
||||||
$paymentURL = '';
|
|
||||||
if (count($paymentTypes)) {
|
|
||||||
$paymentURL = $paymentTypes[0]['url'];
|
|
||||||
if (!$account->isGatewayConfigured(GATEWAY_PAYPAL_EXPRESS)) {
|
|
||||||
$paymentURL = URL::to($paymentURL);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
$showApprove = $invoice->quote_invoice_id ? false : true;
|
|
||||||
if ($invoice->due_date) {
|
|
||||||
$showApprove = time() < strtotime($invoice->due_date);
|
|
||||||
}
|
|
||||||
|
|
||||||
$data = array(
|
|
||||||
'showApprove' => $showApprove,
|
|
||||||
'showBreadcrumbs' => false,
|
|
||||||
'hideLogo' => $account->isWhiteLabel(),
|
|
||||||
'hideHeader' => $account->isNinjaAccount(),
|
|
||||||
'clientViewCSS' => $account->clientViewCSS(),
|
|
||||||
'clientFontUrl' => $account->getFontsUrl(),
|
|
||||||
'invoice' => $invoice->hidePrivateFields(),
|
|
||||||
'invitation' => $invitation,
|
|
||||||
'invoiceLabels' => $account->getInvoiceLabels(),
|
|
||||||
'contact' => $contact,
|
|
||||||
'paymentTypes' => $paymentTypes,
|
|
||||||
'paymentURL' => $paymentURL,
|
|
||||||
'phantomjs' => Input::has('phantomjs'),
|
|
||||||
);
|
|
||||||
|
|
||||||
return View::make('invoices.view', $data);
|
|
||||||
}
|
|
||||||
|
|
||||||
private function getPaymentTypes($client, $invitation)
|
|
||||||
{
|
|
||||||
$paymentTypes = [];
|
|
||||||
$account = $client->account;
|
|
||||||
|
|
||||||
if ($client->getGatewayToken()) {
|
|
||||||
$paymentTypes[] = [
|
|
||||||
'url' => URL::to("payment/{$invitation->invitation_key}/token"), 'label' => trans('texts.use_card_on_file')
|
|
||||||
];
|
|
||||||
}
|
|
||||||
foreach(Gateway::$paymentTypes as $type) {
|
|
||||||
if ($account->getGatewayByType($type)) {
|
|
||||||
$typeLink = strtolower(str_replace('PAYMENT_TYPE_', '', $type));
|
|
||||||
$url = URL::to("/payment/{$invitation->invitation_key}/{$typeLink}");
|
|
||||||
|
|
||||||
// PayPal doesn't allow being run in an iframe so we need to open in new tab
|
|
||||||
if ($type === PAYMENT_TYPE_PAYPAL && $account->iframe_url) {
|
|
||||||
$url = 'javascript:window.open("'.$url.'", "_blank")';
|
|
||||||
}
|
|
||||||
$paymentTypes[] = [
|
|
||||||
'url' => $url, 'label' => trans('texts.'.strtolower($type))
|
|
||||||
];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return $paymentTypes;
|
|
||||||
}
|
|
||||||
|
|
||||||
public function edit($publicId, $clone = false)
|
public function edit($publicId, $clone = false)
|
||||||
{
|
{
|
||||||
$account = Auth::user()->account;
|
$account = Auth::user()->account;
|
||||||
|
@ -532,7 +532,8 @@ class PaymentController extends BaseController
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
if (method_exists($gateway, 'completePurchase')
|
if (method_exists($gateway, 'completePurchase')
|
||||||
&& !$accountGateway->isGateway(GATEWAY_TWO_CHECKOUT)) {
|
&& !$accountGateway->isGateway(GATEWAY_TWO_CHECKOUT)
|
||||||
|
&& !$accountGateway->isGateway(GATEWAY_CHECKOUT_COM)) {
|
||||||
$details = $this->paymentService->getPaymentDetails($invitation, $accountGateway);
|
$details = $this->paymentService->getPaymentDetails($invitation, $accountGateway);
|
||||||
$response = $this->paymentService->completePurchase($gateway, $accountGateway, $details, $token);
|
$response = $this->paymentService->completePurchase($gateway, $accountGateway, $details, $token);
|
||||||
$ref = $response->getTransactionReference() ?: $token;
|
$ref = $response->getTransactionReference() ?: $token;
|
||||||
|
@ -1,25 +1,162 @@
|
|||||||
<?php namespace App\Http\Controllers;
|
<?php namespace App\Http\Controllers;
|
||||||
|
|
||||||
use Auth;
|
use Auth;
|
||||||
|
use View;
|
||||||
use DB;
|
use DB;
|
||||||
|
use URL;
|
||||||
use Input;
|
use Input;
|
||||||
use Utils;
|
use Utils;
|
||||||
|
use Request;
|
||||||
|
use Session;
|
||||||
use Datatable;
|
use Datatable;
|
||||||
|
use App\Models\Gateway;
|
||||||
use App\Models\Invitation;
|
use App\Models\Invitation;
|
||||||
use App\Ninja\Repositories\InvoiceRepository;
|
use App\Ninja\Repositories\InvoiceRepository;
|
||||||
use App\Ninja\Repositories\PaymentRepository;
|
use App\Ninja\Repositories\PaymentRepository;
|
||||||
use App\Ninja\Repositories\ActivityRepository;
|
use App\Ninja\Repositories\ActivityRepository;
|
||||||
|
use App\Events\InvoiceInvitationWasViewed;
|
||||||
|
use App\Events\QuoteInvitationWasViewed;
|
||||||
|
use App\Services\PaymentService;
|
||||||
|
|
||||||
class PublicClientController extends BaseController
|
class PublicClientController extends BaseController
|
||||||
{
|
{
|
||||||
private $invoiceRepo;
|
private $invoiceRepo;
|
||||||
private $paymentRepo;
|
private $paymentRepo;
|
||||||
|
|
||||||
public function __construct(InvoiceRepository $invoiceRepo, PaymentRepository $paymentRepo, ActivityRepository $activityRepo)
|
public function __construct(InvoiceRepository $invoiceRepo, PaymentRepository $paymentRepo, ActivityRepository $activityRepo, PaymentService $paymentService)
|
||||||
{
|
{
|
||||||
$this->invoiceRepo = $invoiceRepo;
|
$this->invoiceRepo = $invoiceRepo;
|
||||||
$this->paymentRepo = $paymentRepo;
|
$this->paymentRepo = $paymentRepo;
|
||||||
$this->activityRepo = $activityRepo;
|
$this->activityRepo = $activityRepo;
|
||||||
|
$this->paymentService = $paymentService;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function view($invitationKey)
|
||||||
|
{
|
||||||
|
if (!$invitation = $this->invoiceRepo->findInvoiceByInvitation($invitationKey)) {
|
||||||
|
return response()->view('error', [
|
||||||
|
'error' => trans('texts.invoice_not_found'),
|
||||||
|
'hideHeader' => true,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
$invoice = $invitation->invoice;
|
||||||
|
$client = $invoice->client;
|
||||||
|
$account = $invoice->account;
|
||||||
|
|
||||||
|
if (!$account->checkSubdomain(Request::server('HTTP_HOST'))) {
|
||||||
|
return response()->view('error', [
|
||||||
|
'error' => trans('texts.invoice_not_found'),
|
||||||
|
'hideHeader' => true,
|
||||||
|
'clientViewCSS' => $account->clientViewCSS(),
|
||||||
|
'clientFontUrl' => $account->getFontsUrl(),
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!Input::has('phantomjs') && !Session::has($invitationKey) && (!Auth::check() || Auth::user()->account_id != $invoice->account_id)) {
|
||||||
|
if ($invoice->is_quote) {
|
||||||
|
event(new QuoteInvitationWasViewed($invoice, $invitation));
|
||||||
|
} else {
|
||||||
|
event(new InvoiceInvitationWasViewed($invoice, $invitation));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Session::put($invitationKey, true); // track this invitation has been seen
|
||||||
|
Session::put('invitation_key', $invitationKey); // track current invitation
|
||||||
|
|
||||||
|
$account->loadLocalizationSettings($client);
|
||||||
|
|
||||||
|
$invoice->invoice_date = Utils::fromSqlDate($invoice->invoice_date);
|
||||||
|
$invoice->due_date = Utils::fromSqlDate($invoice->due_date);
|
||||||
|
$invoice->is_pro = $account->isPro();
|
||||||
|
$invoice->invoice_fonts = $account->getFontsData();
|
||||||
|
|
||||||
|
if ($invoice->invoice_design_id == CUSTOM_DESIGN) {
|
||||||
|
$invoice->invoice_design->javascript = $account->custom_design;
|
||||||
|
} else {
|
||||||
|
$invoice->invoice_design->javascript = $invoice->invoice_design->pdfmake;
|
||||||
|
}
|
||||||
|
$contact = $invitation->contact;
|
||||||
|
$contact->setVisible([
|
||||||
|
'first_name',
|
||||||
|
'last_name',
|
||||||
|
'email',
|
||||||
|
'phone',
|
||||||
|
]);
|
||||||
|
|
||||||
|
$paymentTypes = $this->getPaymentTypes($client, $invitation);
|
||||||
|
$paymentURL = '';
|
||||||
|
if (count($paymentTypes)) {
|
||||||
|
$paymentURL = $paymentTypes[0]['url'];
|
||||||
|
if (!$account->isGatewayConfigured(GATEWAY_PAYPAL_EXPRESS)) {
|
||||||
|
$paymentURL = URL::to($paymentURL);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$showApprove = $invoice->quote_invoice_id ? false : true;
|
||||||
|
if ($invoice->due_date) {
|
||||||
|
$showApprove = time() < strtotime($invoice->due_date);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Checkout.com requires first getting a payment token
|
||||||
|
$checkoutComToken = false;
|
||||||
|
$checkoutComKey = false;
|
||||||
|
if ($accountGateway = $account->getGatewayConfig(GATEWAY_CHECKOUT_COM)) {
|
||||||
|
if ($checkoutComToken = $this->paymentService->getCheckoutComToken($invitation)) {
|
||||||
|
$checkoutComKey = $accountGateway->getConfigField('publicApiKey');
|
||||||
|
$invitation->transaction_reference = $checkoutComToken;
|
||||||
|
$invitation->save();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$data = array(
|
||||||
|
'account' => $account,
|
||||||
|
'showApprove' => $showApprove,
|
||||||
|
'showBreadcrumbs' => false,
|
||||||
|
'hideLogo' => $account->isWhiteLabel(),
|
||||||
|
'hideHeader' => $account->isNinjaAccount(),
|
||||||
|
'clientViewCSS' => $account->clientViewCSS(),
|
||||||
|
'clientFontUrl' => $account->getFontsUrl(),
|
||||||
|
'invoice' => $invoice->hidePrivateFields(),
|
||||||
|
'invitation' => $invitation,
|
||||||
|
'invoiceLabels' => $account->getInvoiceLabels(),
|
||||||
|
'contact' => $contact,
|
||||||
|
'paymentTypes' => $paymentTypes,
|
||||||
|
'paymentURL' => $paymentURL,
|
||||||
|
'checkoutComToken' => $checkoutComToken,
|
||||||
|
'checkoutComKey' => $checkoutComKey,
|
||||||
|
'phantomjs' => Input::has('phantomjs'),
|
||||||
|
);
|
||||||
|
|
||||||
|
return View::make('invoices.view', $data);
|
||||||
|
}
|
||||||
|
|
||||||
|
private function getPaymentTypes($client, $invitation)
|
||||||
|
{
|
||||||
|
$paymentTypes = [];
|
||||||
|
$account = $client->account;
|
||||||
|
|
||||||
|
if ($client->getGatewayToken()) {
|
||||||
|
$paymentTypes[] = [
|
||||||
|
'url' => URL::to("payment/{$invitation->invitation_key}/token"), 'label' => trans('texts.use_card_on_file')
|
||||||
|
];
|
||||||
|
}
|
||||||
|
foreach(Gateway::$paymentTypes as $type) {
|
||||||
|
if ($account->getGatewayByType($type)) {
|
||||||
|
$typeLink = strtolower(str_replace('PAYMENT_TYPE_', '', $type));
|
||||||
|
$url = URL::to("/payment/{$invitation->invitation_key}/{$typeLink}");
|
||||||
|
|
||||||
|
// PayPal doesn't allow being run in an iframe so we need to open in new tab
|
||||||
|
if ($type === PAYMENT_TYPE_PAYPAL && $account->iframe_url) {
|
||||||
|
$url = 'javascript:window.open("'.$url.'", "_blank")';
|
||||||
|
}
|
||||||
|
$paymentTypes[] = [
|
||||||
|
'url' => $url, 'label' => trans('texts.'.strtolower($type))
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return $paymentTypes;
|
||||||
}
|
}
|
||||||
|
|
||||||
public function dashboard()
|
public function dashboard()
|
||||||
|
@ -46,6 +46,11 @@ class Utils
|
|||||||
return file_exists(storage_path() . '/framework/down');
|
return file_exists(storage_path() . '/framework/down');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static function isCron()
|
||||||
|
{
|
||||||
|
return php_sapi_name() == 'cli';
|
||||||
|
}
|
||||||
|
|
||||||
public static function isNinja()
|
public static function isNinja()
|
||||||
{
|
{
|
||||||
return self::isNinjaProd() || self::isNinjaDev();
|
return self::isNinjaProd() || self::isNinjaDev();
|
||||||
|
335
app/Libraries/lib_autolink.php
Normal file
335
app/Libraries/lib_autolink.php
Normal file
@ -0,0 +1,335 @@
|
|||||||
|
<?php
|
||||||
|
#
|
||||||
|
# A PHP auto-linking library
|
||||||
|
#
|
||||||
|
# https://github.com/iamcal/lib_autolink
|
||||||
|
#
|
||||||
|
# By Cal Henderson <cal@iamcal.com>
|
||||||
|
# This code is licensed under the MIT license
|
||||||
|
#
|
||||||
|
|
||||||
|
####################################################################
|
||||||
|
|
||||||
|
#
|
||||||
|
# These are global options. You can set them before calling the autolinking
|
||||||
|
# functions to change the output.
|
||||||
|
#
|
||||||
|
|
||||||
|
$GLOBALS['autolink_options'] = array(
|
||||||
|
|
||||||
|
# Should http:// be visibly stripped from the front
|
||||||
|
# of URLs?
|
||||||
|
'strip_protocols' => false,
|
||||||
|
|
||||||
|
);
|
||||||
|
|
||||||
|
####################################################################
|
||||||
|
|
||||||
|
function autolink($text, $limit=30, $tagfill='', $auto_title = true){
|
||||||
|
|
||||||
|
$text = autolink_do($text, '![a-z][a-z-]+://!i', $limit, $tagfill, $auto_title);
|
||||||
|
$text = autolink_do($text, '!(mailto|skype):!i', $limit, $tagfill, $auto_title);
|
||||||
|
$text = autolink_do($text, '!www\\.!i', $limit, $tagfill, $auto_title, 'http://');
|
||||||
|
return $text;
|
||||||
|
}
|
||||||
|
|
||||||
|
####################################################################
|
||||||
|
|
||||||
|
function autolink_do($text, $sub, $limit, $tagfill, $auto_title, $force_prefix=null){
|
||||||
|
|
||||||
|
$text_l = StrToLower($text);
|
||||||
|
$cursor = 0;
|
||||||
|
$loop = 1;
|
||||||
|
$buffer = '';
|
||||||
|
|
||||||
|
while (($cursor < strlen($text)) && $loop){
|
||||||
|
|
||||||
|
$ok = 1;
|
||||||
|
$matched = preg_match($sub, $text_l, $m, PREG_OFFSET_CAPTURE, $cursor);
|
||||||
|
|
||||||
|
if (!$matched){
|
||||||
|
|
||||||
|
$loop = 0;
|
||||||
|
$ok = 0;
|
||||||
|
|
||||||
|
}else{
|
||||||
|
|
||||||
|
$pos = $m[0][1];
|
||||||
|
$sub_len = strlen($m[0][0]);
|
||||||
|
|
||||||
|
$pre_hit = substr($text, $cursor, $pos-$cursor);
|
||||||
|
$hit = substr($text, $pos, $sub_len);
|
||||||
|
$pre = substr($text, 0, $pos);
|
||||||
|
$post = substr($text, $pos + $sub_len);
|
||||||
|
|
||||||
|
$fail_text = $pre_hit.$hit;
|
||||||
|
$fail_len = strlen($fail_text);
|
||||||
|
|
||||||
|
#
|
||||||
|
# substring found - first check to see if we're inside a link tag already...
|
||||||
|
#
|
||||||
|
|
||||||
|
$bits = preg_split("!</a>!i", $pre);
|
||||||
|
$last_bit = array_pop($bits);
|
||||||
|
if (preg_match("!<a\s!i", $last_bit)){
|
||||||
|
|
||||||
|
#echo "fail 1 at $cursor<br />\n";
|
||||||
|
|
||||||
|
$ok = 0;
|
||||||
|
$cursor += $fail_len;
|
||||||
|
$buffer .= $fail_text;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#
|
||||||
|
# looks like a nice spot to autolink from - check the pre
|
||||||
|
# to see if there was whitespace before this match
|
||||||
|
#
|
||||||
|
|
||||||
|
if ($ok){
|
||||||
|
|
||||||
|
if ($pre){
|
||||||
|
if (!preg_match('![\s\(\[\{>]$!s', $pre)){
|
||||||
|
|
||||||
|
#echo "fail 2 at $cursor ($pre)<br />\n";
|
||||||
|
|
||||||
|
$ok = 0;
|
||||||
|
$cursor += $fail_len;
|
||||||
|
$buffer .= $fail_text;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#
|
||||||
|
# we want to autolink here - find the extent of the url
|
||||||
|
#
|
||||||
|
|
||||||
|
if ($ok){
|
||||||
|
if (preg_match('/^([a-z0-9\-\.\/\-_%~!?=,:;&+*#@\(\)\$]+)/i', $post, $matches)){
|
||||||
|
|
||||||
|
$url = $hit.$matches[1];
|
||||||
|
|
||||||
|
$cursor += strlen($url) + strlen($pre_hit);
|
||||||
|
$buffer .= $pre_hit;
|
||||||
|
|
||||||
|
$url = html_entity_decode($url);
|
||||||
|
|
||||||
|
|
||||||
|
#
|
||||||
|
# remove trailing punctuation from url
|
||||||
|
#
|
||||||
|
|
||||||
|
while (preg_match('|[.,!;:?]$|', $url)){
|
||||||
|
$url = substr($url, 0, strlen($url)-1);
|
||||||
|
$cursor--;
|
||||||
|
}
|
||||||
|
foreach (array('()', '[]', '{}') as $pair){
|
||||||
|
$o = substr($pair, 0, 1);
|
||||||
|
$c = substr($pair, 1, 1);
|
||||||
|
if (preg_match("!^(\\$c|^)[^\\$o]+\\$c$!", $url)){
|
||||||
|
$url = substr($url, 0, strlen($url)-1);
|
||||||
|
$cursor--;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
#
|
||||||
|
# nice-i-fy url here
|
||||||
|
#
|
||||||
|
|
||||||
|
$link_url = $url;
|
||||||
|
$display_url = $url;
|
||||||
|
|
||||||
|
if ($force_prefix) $link_url = $force_prefix.$link_url;
|
||||||
|
|
||||||
|
if ($GLOBALS['autolink_options']['strip_protocols']){
|
||||||
|
if (preg_match('!^(http|https)://!i', $display_url, $m)){
|
||||||
|
|
||||||
|
$display_url = substr($display_url, strlen($m[1])+3);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$display_url = autolink_label($display_url, $limit);
|
||||||
|
|
||||||
|
|
||||||
|
#
|
||||||
|
# add the url
|
||||||
|
#
|
||||||
|
|
||||||
|
$currentTagfill = $tagfill;
|
||||||
|
if ($display_url != $link_url && !preg_match('@title=@msi',$currentTagfill) && $auto_title) {
|
||||||
|
|
||||||
|
$display_quoted = preg_quote($display_url, '!');
|
||||||
|
|
||||||
|
if (!preg_match("!^(http|https)://{$display_quoted}$!i", $link_url)){
|
||||||
|
|
||||||
|
$currentTagfill .= ' title="'.$link_url.'"';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$link_url_enc = HtmlSpecialChars($link_url);
|
||||||
|
$display_url_enc = HtmlSpecialChars($display_url);
|
||||||
|
|
||||||
|
$buffer .= "<a href=\"{$link_url_enc}\"$currentTagfill>{$display_url_enc}</a>";
|
||||||
|
|
||||||
|
}else{
|
||||||
|
#echo "fail 3 at $cursor<br />\n";
|
||||||
|
|
||||||
|
$ok = 0;
|
||||||
|
$cursor += $fail_len;
|
||||||
|
$buffer .= $fail_text;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
#
|
||||||
|
# add everything from the cursor to the end onto the buffer.
|
||||||
|
#
|
||||||
|
|
||||||
|
$buffer .= substr($text, $cursor);
|
||||||
|
|
||||||
|
return $buffer;
|
||||||
|
}
|
||||||
|
|
||||||
|
####################################################################
|
||||||
|
|
||||||
|
function autolink_label($text, $limit){
|
||||||
|
|
||||||
|
if (!$limit){ return $text; }
|
||||||
|
|
||||||
|
if (strlen($text) > $limit){
|
||||||
|
return substr($text, 0, $limit-3).'...';
|
||||||
|
}
|
||||||
|
|
||||||
|
return $text;
|
||||||
|
}
|
||||||
|
|
||||||
|
####################################################################
|
||||||
|
|
||||||
|
function autolink_email($text, $tagfill=''){
|
||||||
|
|
||||||
|
$atom = '[^()<>@,;:\\\\".\\[\\]\\x00-\\x20\\x7f]+'; # from RFC822
|
||||||
|
|
||||||
|
#die($atom);
|
||||||
|
|
||||||
|
$text_l = StrToLower($text);
|
||||||
|
$cursor = 0;
|
||||||
|
$loop = 1;
|
||||||
|
$buffer = '';
|
||||||
|
|
||||||
|
while(($cursor < strlen($text)) && $loop){
|
||||||
|
|
||||||
|
#
|
||||||
|
# find an '@' symbol
|
||||||
|
#
|
||||||
|
|
||||||
|
$ok = 1;
|
||||||
|
$pos = strpos($text_l, '@', $cursor);
|
||||||
|
|
||||||
|
if ($pos === false){
|
||||||
|
|
||||||
|
$loop = 0;
|
||||||
|
$ok = 0;
|
||||||
|
|
||||||
|
}else{
|
||||||
|
|
||||||
|
$pre = substr($text, $cursor, $pos-$cursor);
|
||||||
|
$hit = substr($text, $pos, 1);
|
||||||
|
$post = substr($text, $pos + 1);
|
||||||
|
|
||||||
|
$fail_text = $pre.$hit;
|
||||||
|
$fail_len = strlen($fail_text);
|
||||||
|
|
||||||
|
#die("$pre::$hit::$post::$fail_text");
|
||||||
|
|
||||||
|
#
|
||||||
|
# substring found - first check to see if we're inside a link tag already...
|
||||||
|
#
|
||||||
|
|
||||||
|
$bits = preg_split("!</a>!i", $pre);
|
||||||
|
$last_bit = array_pop($bits);
|
||||||
|
if (preg_match("!<a\s!i", $last_bit)){
|
||||||
|
|
||||||
|
#echo "fail 1 at $cursor<br />\n";
|
||||||
|
|
||||||
|
$ok = 0;
|
||||||
|
$cursor += $fail_len;
|
||||||
|
$buffer .= $fail_text;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#
|
||||||
|
# check backwards
|
||||||
|
#
|
||||||
|
|
||||||
|
if ($ok){
|
||||||
|
if (preg_match("!($atom(\.$atom)*)\$!", $pre, $matches)){
|
||||||
|
|
||||||
|
# move matched part of address into $hit
|
||||||
|
|
||||||
|
$len = strlen($matches[1]);
|
||||||
|
$plen = strlen($pre);
|
||||||
|
|
||||||
|
$hit = substr($pre, $plen-$len).$hit;
|
||||||
|
$pre = substr($pre, 0, $plen-$len);
|
||||||
|
|
||||||
|
}else{
|
||||||
|
|
||||||
|
#echo "fail 2 at $cursor ($pre)<br />\n";
|
||||||
|
|
||||||
|
$ok = 0;
|
||||||
|
$cursor += $fail_len;
|
||||||
|
$buffer .= $fail_text;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#
|
||||||
|
# check forwards
|
||||||
|
#
|
||||||
|
|
||||||
|
if ($ok){
|
||||||
|
if (preg_match("!^($atom(\.$atom)*)!", $post, $matches)){
|
||||||
|
|
||||||
|
# move matched part of address into $hit
|
||||||
|
|
||||||
|
$len = strlen($matches[1]);
|
||||||
|
|
||||||
|
$hit .= substr($post, 0, $len);
|
||||||
|
$post = substr($post, $len);
|
||||||
|
|
||||||
|
}else{
|
||||||
|
#echo "fail 3 at $cursor ($post)<br />\n";
|
||||||
|
|
||||||
|
$ok = 0;
|
||||||
|
$cursor += $fail_len;
|
||||||
|
$buffer .= $fail_text;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#
|
||||||
|
# commit
|
||||||
|
#
|
||||||
|
|
||||||
|
if ($ok) {
|
||||||
|
|
||||||
|
$cursor += strlen($pre) + strlen($hit);
|
||||||
|
$buffer .= $pre;
|
||||||
|
$buffer .= "<a href=\"mailto:$hit\"$tagfill>$hit</a>";
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
#
|
||||||
|
# add everything from the cursor to the end onto the buffer.
|
||||||
|
#
|
||||||
|
|
||||||
|
$buffer .= substr($text, $cursor);
|
||||||
|
|
||||||
|
return $buffer;
|
||||||
|
}
|
||||||
|
|
||||||
|
####################################################################
|
||||||
|
|
||||||
|
?>
|
@ -260,7 +260,8 @@ class ContactMailer extends Mailer
|
|||||||
}
|
}
|
||||||
|
|
||||||
$str = str_replace(array_keys($variables), array_values($variables), $template);
|
$str = str_replace(array_keys($variables), array_values($variables), $template);
|
||||||
|
$str = autolink($str, 100);
|
||||||
|
|
||||||
return $str;
|
return $str;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -62,7 +62,7 @@ class AppServiceProvider extends ServiceProvider {
|
|||||||
});
|
});
|
||||||
|
|
||||||
HTML::macro('image_data', function($imagePath) {
|
HTML::macro('image_data', function($imagePath) {
|
||||||
return 'data:image/jpeg;base64,' . base64_encode(file_get_contents(public_path().'/'.$imagePath));
|
return 'data:image/jpeg;base64,' . base64_encode(file_get_contents($imagePath));
|
||||||
});
|
});
|
||||||
|
|
||||||
HTML::macro('flatButton', function($label, $color) {
|
HTML::macro('flatButton', function($label, $color) {
|
||||||
|
@ -184,6 +184,30 @@ class PaymentService extends BaseService
|
|||||||
return $cardReference;
|
return $cardReference;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function getCheckoutComToken($invitation)
|
||||||
|
{
|
||||||
|
$token = false;
|
||||||
|
$invoice = $invitation->invoice;
|
||||||
|
$client = $invoice->client;
|
||||||
|
$account = $invoice->account;
|
||||||
|
|
||||||
|
$accountGateway = $account->getGatewayConfig(GATEWAY_CHECKOUT_COM);
|
||||||
|
$gateway = $this->createGateway($accountGateway);
|
||||||
|
|
||||||
|
$response = $gateway->purchase([
|
||||||
|
'amount' => $invoice->getRequestedAmount(),
|
||||||
|
'currency' => $client->currency ? $client->currency->code : ($account->currency ? $account->currency->code : 'USD')
|
||||||
|
])->send();
|
||||||
|
|
||||||
|
if ($response->isRedirect()) {
|
||||||
|
$token = $response->getTransactionReference();
|
||||||
|
}
|
||||||
|
|
||||||
|
Session::set($invitation->id . 'payment_type', PAYMENT_TYPE_CREDIT_CARD);
|
||||||
|
|
||||||
|
return $token;
|
||||||
|
}
|
||||||
|
|
||||||
public function createPayment($invitation, $accountGateway, $ref, $payerId = null)
|
public function createPayment($invitation, $accountGateway, $ref, $payerId = null)
|
||||||
{
|
{
|
||||||
$invoice = $invitation->invoice;
|
$invoice = $invitation->invoice;
|
||||||
|
@ -86,7 +86,10 @@
|
|||||||
],
|
],
|
||||||
"psr-4": {
|
"psr-4": {
|
||||||
"App\\": "app/"
|
"App\\": "app/"
|
||||||
}
|
},
|
||||||
|
"files": [
|
||||||
|
"app/Libraries/lib_autolink.php"
|
||||||
|
]
|
||||||
},
|
},
|
||||||
"autoload-dev": {
|
"autoload-dev": {
|
||||||
"classmap": [
|
"classmap": [
|
||||||
|
@ -110,7 +110,7 @@ class PaymentLibrariesSeeder extends Seeder
|
|||||||
['name' => 'Argentine Peso', 'code' => 'ARS', 'symbol' => '$', 'precision' => '2', 'thousand_separator' => '.', 'decimal_separator' => ','],
|
['name' => 'Argentine Peso', 'code' => 'ARS', 'symbol' => '$', 'precision' => '2', 'thousand_separator' => '.', 'decimal_separator' => ','],
|
||||||
['name' => 'Bangladeshi Taka', 'code' => 'BDT', 'symbol' => 'Tk', 'precision' => '2', 'thousand_separator' => ',', 'decimal_separator' => '.'],
|
['name' => 'Bangladeshi Taka', 'code' => 'BDT', 'symbol' => 'Tk', 'precision' => '2', 'thousand_separator' => ',', 'decimal_separator' => '.'],
|
||||||
['name' => 'United Arab Emirates Dirham', 'code' => 'AED', 'symbol' => 'DH ', 'precision' => '2', 'thousand_separator' => ',', 'decimal_separator' => '.'],
|
['name' => 'United Arab Emirates Dirham', 'code' => 'AED', 'symbol' => 'DH ', 'precision' => '2', 'thousand_separator' => ',', 'decimal_separator' => '.'],
|
||||||
['name' => 'Hong Kong Dollar', 'code' => 'HKD', 'symbol' => '$', 'precision' => '2', 'thousand_separator' => ',', 'decimal_separator' => '.'],
|
['name' => 'Hong Kong Dollar', 'code' => 'HKD', 'symbol' => 'HKD ', 'precision' => '2', 'thousand_separator' => ',', 'decimal_separator' => '.'],
|
||||||
['name' => 'Indonesian Rupiah', 'code' => 'IDR', 'symbol' => 'Rp', 'precision' => '2', 'thousand_separator' => ',', 'decimal_separator' => '.'],
|
['name' => 'Indonesian Rupiah', 'code' => 'IDR', 'symbol' => 'Rp', 'precision' => '2', 'thousand_separator' => ',', 'decimal_separator' => '.'],
|
||||||
['name' => 'Mexican Peso', 'code' => 'MXN', 'symbol' => '$', 'precision' => '2', 'thousand_separator' => ',', 'decimal_separator' => '.'],
|
['name' => 'Mexican Peso', 'code' => 'MXN', 'symbol' => '$', 'precision' => '2', 'thousand_separator' => ',', 'decimal_separator' => '.'],
|
||||||
['name' => 'Egyptian Pound', 'code' => 'EGP', 'symbol' => 'E£', 'precision' => '2', 'thousand_separator' => ',', 'decimal_separator' => '.'],
|
['name' => 'Egyptian Pound', 'code' => 'EGP', 'symbol' => 'E£', 'precision' => '2', 'thousand_separator' => ',', 'decimal_separator' => '.'],
|
||||||
|
@ -5,8 +5,6 @@
|
|||||||
# Invoice Ninja
|
# Invoice Ninja
|
||||||
### [https://www.invoiceninja.com](https://www.invoiceninja.com)
|
### [https://www.invoiceninja.com](https://www.invoiceninja.com)
|
||||||
|
|
||||||
We're starting to work on our expenses, vendors and receipts module. If you'd like to help us design it please send us an email to join the discussion.
|
|
||||||
|
|
||||||
[](https://gitter.im/hillelcoren/invoice-ninja?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge)
|
[](https://gitter.im/hillelcoren/invoice-ninja?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge)
|
||||||
|
|
||||||
### Referral Program
|
### Referral Program
|
||||||
|
@ -3,7 +3,7 @@
|
|||||||
<a href="{{ $account->website }}" style="color: #19BB40; text-decoration: underline;">
|
<a href="{{ $account->website }}" style="color: #19BB40; text-decoration: underline;">
|
||||||
@endif
|
@endif
|
||||||
|
|
||||||
<img src="{{ $message->embed($account->getLogoPath()) }}" style="max-height:50px; max-width:140px; margin-left: 33px;" />
|
<img src="{{ $message->embed($account->getAbsoluteLogoPath()) }}" style="max-height:50px; max-width:140px; margin-left: 33px;" />
|
||||||
|
|
||||||
@if ($account->website)
|
@if ($account->website)
|
||||||
</a>
|
</a>
|
||||||
|
@ -21,24 +21,28 @@
|
|||||||
|
|
||||||
<div class="container">
|
<div class="container">
|
||||||
|
|
||||||
<p> </p>
|
@if ($checkoutComToken)
|
||||||
<div class="pull-right" style="text-align:right">
|
@include('partials.checkout_com_payment')
|
||||||
@if ($invoice->is_quote)
|
@else
|
||||||
{!! Button::normal(trans('texts.download_pdf'))->withAttributes(['onclick' => 'onDownloadClick()'])->large() !!}
|
<p> </p>
|
||||||
@if ($showApprove)
|
<div class="pull-right" style="text-align:right">
|
||||||
{!! Button::success(trans('texts.approve'))->asLinkTo(URL::to('/approve/' . $invitation->invitation_key))->large() !!}
|
@if ($invoice->is_quote)
|
||||||
@endif
|
{!! Button::normal(trans('texts.download_pdf'))->withAttributes(['onclick' => 'onDownloadClick()'])->large() !!}
|
||||||
@elseif ($invoice->client->account->isGatewayConfigured() && !$invoice->isPaid() && !$invoice->is_recurring)
|
@if ($showApprove)
|
||||||
{!! Button::normal(trans('texts.download_pdf'))->withAttributes(['onclick' => 'onDownloadClick()'])->large() !!}
|
{!! Button::success(trans('texts.approve'))->asLinkTo(URL::to('/approve/' . $invitation->invitation_key))->large() !!}
|
||||||
@if (count($paymentTypes) > 1)
|
@endif
|
||||||
{!! DropdownButton::success(trans('texts.pay_now'))->withContents($paymentTypes)->large() !!}
|
@elseif ($invoice->client->account->isGatewayConfigured() && !$invoice->isPaid() && !$invoice->is_recurring)
|
||||||
@else
|
{!! Button::normal(trans('texts.download_pdf'))->withAttributes(['onclick' => 'onDownloadClick()'])->large() !!}
|
||||||
<a href='{!! $paymentURL !!}' class="btn btn-success btn-lg">{{ trans('texts.pay_now') }}</a>
|
@if (count($paymentTypes) > 1)
|
||||||
@endif
|
{!! DropdownButton::success(trans('texts.pay_now'))->withContents($paymentTypes)->large() !!}
|
||||||
@else
|
@else
|
||||||
{!! Button::normal('Download PDF')->withAttributes(['onclick' => 'onDownloadClick()'])->large() !!}
|
<a href='{!! $paymentURL !!}' class="btn btn-success btn-lg">{{ trans('texts.pay_now') }}</a>
|
||||||
@endif
|
@endif
|
||||||
</div>
|
@else
|
||||||
|
{!! Button::normal('Download PDF')->withAttributes(['onclick' => 'onDownloadClick()'])->large() !!}
|
||||||
|
@endif
|
||||||
|
</div>
|
||||||
|
@endif
|
||||||
|
|
||||||
<div class="clearfix"></div><p> </p>
|
<div class="clearfix"></div><p> </p>
|
||||||
|
|
||||||
|
22
resources/views/partials/checkout_com_payment.blade.php
Normal file
22
resources/views/partials/checkout_com_payment.blade.php
Normal file
@ -0,0 +1,22 @@
|
|||||||
|
<script src="https://sandbox.checkout.com/js/v1/checkout.js"></script>
|
||||||
|
|
||||||
|
<form method="POST" class="payment-form">
|
||||||
|
<script>
|
||||||
|
Checkout.render({
|
||||||
|
debugMode: true,
|
||||||
|
publicKey: '{{ $checkoutComKey }}',
|
||||||
|
paymentToken: '{{ $checkoutComToken }}',
|
||||||
|
customerEmail: '{{ $contact->email }}',
|
||||||
|
customerName: '{{ $contact->getFullName() }}',
|
||||||
|
value: {{ $invoice->getRequestedAmount() * 100 }},
|
||||||
|
currency: '{{ $invoice->getCurrencyCode() }}',
|
||||||
|
widgetContainerSelector: '.payment-form',
|
||||||
|
widgetColor: '#333',
|
||||||
|
themeColor: '#3075dd',
|
||||||
|
buttonColor:'#51c470',
|
||||||
|
cardCharged: function(event){
|
||||||
|
location.href = '{{ URL::to('/complete?token=' . $checkoutComToken) }}';
|
||||||
|
}
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
</form>
|
103
tests/acceptance/TaxRatesCest.php
Normal file
103
tests/acceptance/TaxRatesCest.php
Normal file
@ -0,0 +1,103 @@
|
|||||||
|
/<?php
|
||||||
|
|
||||||
|
use Codeception\Util\Fixtures;
|
||||||
|
use \AcceptanceTester;
|
||||||
|
use Faker\Factory;
|
||||||
|
|
||||||
|
class TaxRatesCest
|
||||||
|
{
|
||||||
|
private $faker;
|
||||||
|
|
||||||
|
public function _before(AcceptanceTester $I)
|
||||||
|
{
|
||||||
|
$I->checkIfLogin($I);
|
||||||
|
|
||||||
|
$this->faker = Factory::create();
|
||||||
|
}
|
||||||
|
|
||||||
|
public function taxRates(AcceptanceTester $I)
|
||||||
|
{
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
public function onlinePayment(AcceptanceTester $I)
|
||||||
|
{
|
||||||
|
$I->wantTo('test an online payment');
|
||||||
|
|
||||||
|
$clientEmail = $this->faker->safeEmail;
|
||||||
|
$productKey = $this->faker->text(10);
|
||||||
|
|
||||||
|
// set gateway info
|
||||||
|
$I->wantTo('create a gateway');
|
||||||
|
$I->amOnPage('/settings/online_payments');
|
||||||
|
|
||||||
|
if (strpos($I->grabFromCurrentUrl(), 'create') !== false) {
|
||||||
|
$I->fillField(['name' =>'23_apiKey'], Fixtures::get('gateway_key'));
|
||||||
|
$I->selectOption('#token_billing_type_id', 4);
|
||||||
|
$I->click('Save');
|
||||||
|
$I->see('Successfully created gateway');
|
||||||
|
}
|
||||||
|
|
||||||
|
// create client
|
||||||
|
$I->amOnPage('/clients/create');
|
||||||
|
$I->fillField(['name' => 'contacts[0][email]'], $clientEmail);
|
||||||
|
$I->click('Save');
|
||||||
|
$I->see($clientEmail);
|
||||||
|
|
||||||
|
// create product
|
||||||
|
$I->amOnPage('/products/create');
|
||||||
|
$I->fillField(['name' => 'product_key'], $productKey);
|
||||||
|
$I->fillField(['name' => 'notes'], $this->faker->text(80));
|
||||||
|
$I->fillField(['name' => 'cost'], $this->faker->numberBetween(1, 20));
|
||||||
|
$I->click('Save');
|
||||||
|
$I->wait(1);
|
||||||
|
$I->see($productKey);
|
||||||
|
|
||||||
|
// create invoice
|
||||||
|
$I->amOnPage('/invoices/create');
|
||||||
|
$I->selectDropdown($I, $clientEmail, '.client_select .dropdown-toggle');
|
||||||
|
$I->fillField('table.invoice-table tbody tr:nth-child(1) #product_key', $productKey);
|
||||||
|
$I->click('Save');
|
||||||
|
$I->see($clientEmail);
|
||||||
|
|
||||||
|
// enter payment
|
||||||
|
$clientId = $I->grabFromDatabase('contacts', 'client_id', ['email' => $clientEmail]);
|
||||||
|
$invoiceId = $I->grabFromDatabase('invoices', 'id', ['client_id' => $clientId]);
|
||||||
|
$invitationKey = $I->grabFromDatabase('invitations', 'invitation_key', ['invoice_id' => $invoiceId]);
|
||||||
|
|
||||||
|
$clientSession = $I->haveFriend('client');
|
||||||
|
$clientSession->does(function(AcceptanceTester $I) use ($invitationKey) {
|
||||||
|
$I->amOnPage('/view/' . $invitationKey);
|
||||||
|
$I->click('Pay Now');
|
||||||
|
|
||||||
|
$I->fillField(['name' => 'first_name'], $this->faker->firstName);
|
||||||
|
$I->fillField(['name' => 'last_name'], $this->faker->lastName);
|
||||||
|
$I->fillField(['name' => 'address1'], $this->faker->streetAddress);
|
||||||
|
$I->fillField(['name' => 'address2'], $this->faker->streetAddress);
|
||||||
|
$I->fillField(['name' => 'city'], $this->faker->city);
|
||||||
|
$I->fillField(['name' => 'state'], $this->faker->state);
|
||||||
|
$I->fillField(['name' => 'postal_code'], $this->faker->postcode);
|
||||||
|
$I->selectDropdown($I, 'United States', '.country-select .dropdown-toggle');
|
||||||
|
$I->fillField(['name' => 'card_number'], '4242424242424242');
|
||||||
|
$I->fillField(['name' => 'cvv'], '1234');
|
||||||
|
$I->selectOption('#expiration_month', 12);
|
||||||
|
$I->selectOption('#expiration_year', date('Y'));
|
||||||
|
$I->click('.btn-success');
|
||||||
|
$I->see('Successfully applied payment');
|
||||||
|
});
|
||||||
|
|
||||||
|
$I->wait(1);
|
||||||
|
|
||||||
|
// create recurring invoice and auto-bill
|
||||||
|
$I->amOnPage('/recurring_invoices/create');
|
||||||
|
$I->selectDropdown($I, $clientEmail, '.client_select .dropdown-toggle');
|
||||||
|
$I->fillField('table.invoice-table tbody tr:nth-child(1) #product_key', $productKey);
|
||||||
|
$I->checkOption('#auto_bill');
|
||||||
|
$I->executeJS('preparePdfData(\'email\')');
|
||||||
|
$I->wait(2);
|
||||||
|
$I->see("$0.00");
|
||||||
|
|
||||||
|
}
|
||||||
|
*/
|
||||||
|
}
|
Loading…
x
Reference in New Issue
Block a user