diff --git a/.env.example b/.env.example index e29e72e0dd67..c7cd8ea6d6d9 100644 --- a/.env.example +++ b/.env.example @@ -74,5 +74,9 @@ WEPAY_CLIENT_SECRET= WEPAY_AUTO_UPDATE=true # Requires permission from WePay WEPAY_ENVIRONMENT=production # production or stage +WEPAY_FEE_PAYER=payee +WEPAY_APP_FEE_MULTIPLIER=0.002 +WEPAY_APP_FEE_FIXED=0 + # See https://www.wepay.com/developer/reference/structures#theme WEPAY_THEME='{"name":"Invoice Ninja","primary_color":"0b4d78","secondary_color":"0b4d78","background_color":"f8f8f8","button_color":"33b753"}' diff --git a/app/Http/Controllers/AccountGatewayController.php b/app/Http/Controllers/AccountGatewayController.php index fef9a511636e..9b02bfe56a24 100644 --- a/app/Http/Controllers/AccountGatewayController.php +++ b/app/Http/Controllers/AccountGatewayController.php @@ -369,14 +369,20 @@ class AccountGatewayController extends BaseController $user = Auth::user(); $account = $user->account; - $validator = Validator::make(Input::all(), array( + $rules = array( 'company_name' => 'required', 'description' => 'required', 'tos_agree' => 'required', 'first_name' => 'required', 'last_name' => 'required', 'email' => 'required', - )); + ); + + if (WEPAY_ENABLE_CANADA) { + $rules['country'] = 'required|in:US,CA'; + } + + $validator = Validator::make(Input::all(), $rules); if ($validator->fails()) { return Redirect::to('gateways/create') @@ -387,7 +393,7 @@ class AccountGatewayController extends BaseController try{ $wepay = Utils::setupWePay(); - $wepayUser = $wepay->request('user/register/', array( + $userDetails = array( 'client_id' => WEPAY_CLIENT_ID, 'client_secret' => WEPAY_CLIENT_SECRET, 'email' => Input::get('email'), @@ -399,18 +405,31 @@ class AccountGatewayController extends BaseController 'redirect_uri' => URL::to('gateways'), 'callback_uri' => URL::to(env('WEBHOOK_PREFIX','').'paymenthook/'.$account->account_key.'/'.GATEWAY_WEPAY), 'scope' => 'manage_accounts,collect_payments,view_user,preapprove_payments,send_money', - )); + ); + + $wepayUser = $wepay->request('user/register/', $userDetails); $accessToken = $wepayUser->access_token; $accessTokenExpires = $wepayUser->expires_in ? (time() + $wepayUser->expires_in) : null; $wepay = new WePay($accessToken); - $wepayAccount = $wepay->request('account/create/', array( + $accountDetails = array( 'name' => Input::get('company_name'), 'description' => Input::get('description'), 'theme_object' => json_decode(WEPAY_THEME), - )); + ); + + if (WEPAY_ENABLE_CANADA) { + $accountDetails['country'] = Input::get('country'); + + if (Input::get('country') == 'CA') { + $accountDetails['currencies'] = ['CAD']; + $accountDetails['country_options'] = ['debit_opt_in' => boolval(Input::get('debit_cards'))]; + } + } + + $wepayAccount = $wepay->request('account/create/', $accountDetails); try { $wepay->request('user/send_confirmation/', []); @@ -435,6 +454,7 @@ class AccountGatewayController extends BaseController 'tokenExpires' => $accessTokenExpires, 'accountId' => $wepayAccount->account_id, 'testMode' => WEPAY_ENVIRONMENT == WEPAY_STAGE, + 'country' => WEPAY_ENABLE_CANADA ? Input::get('country') : 'US', )); if ($confirmationRequired) { diff --git a/app/Http/Controllers/PaymentController.php b/app/Http/Controllers/PaymentController.php index 69553f58d5c1..3e89f3091910 100644 --- a/app/Http/Controllers/PaymentController.php +++ b/app/Http/Controllers/PaymentController.php @@ -328,9 +328,8 @@ class PaymentController extends BaseController if ($testMode) { $ref = 'TEST_MODE'; } else { - $gateway = $this->paymentService->createGateway($accountGateway); $details = self::getLicensePaymentDetails(Input::all(), $affiliate); - $response = $gateway->purchase($details)->send(); + $response = $this->paymentService->purchase($accountGateway, $details); $ref = $response->getTransactionReference(); if (!$response->isSuccessful() || !$ref) { @@ -552,7 +551,7 @@ class PaymentController extends BaseController } } - $response = $gateway->purchase($details)->send(); + $response = $this->paymentService->purchase($accountGateway, $details); if ($accountGateway->gateway_id == GATEWAY_EWAY) { $ref = $response->getData()['AccessCode']; diff --git a/app/Http/routes.php b/app/Http/routes.php index a6002f5d8df3..7a2fae197e59 100644 --- a/app/Http/routes.php +++ b/app/Http/routes.php @@ -123,7 +123,7 @@ Route::group(['middleware' => 'auth:user'], function() { Route::get('hide_message', 'HomeController@hideMessage'); Route::get('force_inline_pdf', 'UserController@forcePDFJS'); Route::get('account/getSearchData', array('as' => 'getSearchData', 'uses' => 'AccountController@getSearchData')); - + Route::get('settings/user_details', 'AccountController@showUserDetails'); Route::post('settings/user_details', 'AccountController@saveUserDetails'); Route::post('users/change_password', 'UserController@changePassword'); @@ -156,7 +156,7 @@ Route::group(['middleware' => 'auth:user'], function() { Route::get('documents/js/{documents}/{filename}', 'DocumentController@getVFSJS'); Route::get('documents/preview/{documents}/{filename?}', 'DocumentController@getPreview'); Route::post('document', 'DocumentController@postUpload'); - + Route::get('quotes/create/{client_id?}', 'QuoteController@create'); Route::get('quotes/{invoices}/clone', 'InvoiceController@cloneInvoice'); Route::get('quotes/{invoices}/edit', 'InvoiceController@edit'); @@ -247,8 +247,6 @@ Route::group([ Route::get('api/gateways', array('as'=>'api.gateways', 'uses'=>'AccountGatewayController@getDatatable')); Route::post('account_gateways/bulk', 'AccountGatewayController@bulk'); - Route::get('bank_accounts/import_ofx', 'BankAccountController@showImportOFX'); - Route::post('bank_accounts/import_ofx', 'BankAccountController@doImportOFX'); Route::resource('bank_accounts', 'BankAccountController'); Route::get('api/bank_accounts', array('as'=>'api.bank_accounts', 'uses'=>'BankAccountController@getDatatable')); Route::post('bank_accounts/bulk', 'BankAccountController@bulk'); @@ -491,7 +489,7 @@ if (!defined('CONTACT_EMAIL')) { define('INVOICE_STATUS_APPROVED', 4); define('INVOICE_STATUS_PARTIAL', 5); define('INVOICE_STATUS_PAID', 6); - + define('PAYMENT_STATUS_PENDING', 1); define('PAYMENT_STATUS_VOIDED', 2); define('PAYMENT_STATUS_FAILED', 3); @@ -713,7 +711,7 @@ if (!defined('CONTACT_EMAIL')) { define('AUTO_BILL_OPT_IN', 2); define('AUTO_BILL_OPT_OUT', 3); define('AUTO_BILL_ALWAYS', 4); - + // These must be lowercase define('PLAN_FREE', 'free'); define('PLAN_PRO', 'pro'); @@ -721,7 +719,7 @@ if (!defined('CONTACT_EMAIL')) { define('PLAN_WHITE_LABEL', 'white_label'); define('PLAN_TERM_MONTHLY', 'month'); define('PLAN_TERM_YEARLY', 'year'); - + // Pro define('FEATURE_CUSTOMIZE_INVOICE_DESIGN', 'customize_invoice_design'); define('FEATURE_REMOVE_CREATED_BY', 'remove_created_by'); @@ -760,8 +758,13 @@ if (!defined('CONTACT_EMAIL')) { define('WEPAY_CLIENT_SECRET', env('WEPAY_CLIENT_SECRET')); define('WEPAY_AUTO_UPDATE', env('WEPAY_AUTO_UPDATE', false)); define('WEPAY_ENVIRONMENT', env('WEPAY_ENVIRONMENT', WEPAY_PRODUCTION)); + define('WEPAY_ENABLE_CANADA', env('WEPAY_ENABLE_CANADA', false)); define('WEPAY_THEME', env('WEPAY_THEME','{"name":"Invoice Ninja","primary_color":"0b4d78","secondary_color":"0b4d78","background_color":"f8f8f8","button_color":"33b753"}')); + define('WEPAY_FEE_PAYER', env('WEPAY_FEE_PAYER', 'payee')); + define('WEPAY_APP_FEE_MULTIPLIER', env('WEPAY_APP_FEE_MULTIPLIER', 0.002)); + define('WEPAY_APP_FEE_FIXED', env('WEPAY_APP_FEE_MULTIPLIER', 0.00)); + $creditCards = [ 1 => ['card' => 'images/credit_cards/Test-Visa-Icon.png', 'text' => 'Visa'], 2 => ['card' => 'images/credit_cards/Test-MasterCard-Icon.png', 'text' => 'Master Card'], @@ -817,4 +820,4 @@ if (Utils::isNinjaDev()) //ini_set('memory_limit','1024M'); //Auth::loginUsingId(1); } -*/ +*/ \ No newline at end of file diff --git a/app/Services/PaymentService.php b/app/Services/PaymentService.php index d1a0a29a7154..ae014577a603 100644 --- a/app/Services/PaymentService.php +++ b/app/Services/PaymentService.php @@ -602,9 +602,8 @@ class PaymentService extends BaseService $account = $invoice->account; $accountGateway = $account->getGatewayConfig(GATEWAY_CHECKOUT_COM); - $gateway = $this->createGateway($accountGateway); - $response = $gateway->purchase([ + $response = $this->purchase($accountGateway, [ 'amount' => $invoice->getRequestedAmount(), 'currency' => $client->currency ? $client->currency->code : ($account->currency ? $account->currency->code : 'USD') ])->send(); @@ -836,7 +835,6 @@ class PaymentService extends BaseService } // setup the gateway/payment info - $gateway = $this->createGateway($accountGateway); $details = $this->getPaymentDetails($invitation, $accountGateway); $details['customerReference'] = $token; @@ -846,7 +844,7 @@ class PaymentService extends BaseService } // submit purchase/get response - $response = $gateway->purchase($details)->send(); + $response = $this->purchase($accountGateway, $details); if ($response->isSuccessful()) { $ref = $response->getTransactionReference(); @@ -994,7 +992,7 @@ class PaymentService extends BaseService $amount = !empty($params['amount']) ? floatval($params['amount']) : null; if ($this->refund($payment, $amount)) { $successful++; - } + } } } @@ -1033,11 +1031,9 @@ class PaymentService extends BaseService } public function refund($payment, $amount = null) { - if (!$amount) { - $amount = $payment->amount; + if ($amount) { + $amount = min($amount, $payment->amount - $payment->refunded); } - - $amount = min($amount, $payment->amount - $payment->refunded); $accountGateway = $payment->account_gateway; @@ -1052,75 +1048,56 @@ class PaymentService extends BaseService if ($payment->payment_type_id != PAYMENT_TYPE_CREDIT) { $gateway = $this->createGateway($accountGateway); - if ($accountGateway->gateway_id != GATEWAY_WEPAY) { - $refund = $gateway->refund(array( - 'transactionReference' => $payment->transaction_reference, - 'amount' => $amount, - )); - $response = $refund->send(); + $details = array( + 'transactionReference' => $payment->transaction_reference, + ); - if ($response->isSuccessful()) { - $payment->recordRefund($amount); - } else { - $data = $response->getData(); + if ($amount != ($payment->amount - $payment->refunded)) { + $details['amount'] = $amount; + } - if ($data instanceof \Braintree\Result\Error) { - $error = $data->errors->deepAll()[0]; - if ($error && $error->code == 91506) { - if ($amount == $payment->amount) { - // This is an unsettled transaction; try to void it - $void = $gateway->void(array( - 'transactionReference' => $payment->transaction_reference, - )); - $response = $void->send(); + if ($accountGateway->gateway_id == GATEWAY_WEPAY) { + $details['refund_reason'] = 'Refund issued by merchant.'; + } - if ($response->isSuccessful()) { - $payment->markVoided(); - } - } else { - $this->error('Unknown', 'Partial refund not allowed for unsettled transactions.', $accountGateway); - return false; - } - } + $refund = $gateway->refund($details); + $response = $refund->send(); + + if ($response->isSuccessful()) { + $payment->recordRefund($amount); + } else { + $data = $response->getData(); + + if ($data instanceof \Braintree\Result\Error) { + $error = $data->errors->deepAll()[0]; + if ($error && $error->code == 91506) { + $tryVoid = true; } + } elseif ($accountGateway->gateway_id == GATEWAY_WEPAY && $response->getCode() == 4004) { + $tryVoid = true; + } - if (!$response->isSuccessful()) { - $this->error('Unknown', $response->getMessage(), $accountGateway); + if (!empty($tryVoid)) { + if ($amount == $payment->amount) { + // This is an unsettled transaction; try to void it + $void = $gateway->void(array( + 'transactionReference' => $payment->transaction_reference, + )); + $response = $void->send(); + + if ($response->isSuccessful()) { + $payment->markVoided(); + } + } else { + $this->error('Unknown', 'Partial refund not allowed for unsettled transactions.', $accountGateway); return false; } } - } else { - $wepay = \Utils::setupWePay($accountGateway); - try { - $wepay->request('checkout/refund', array( - 'checkout_id' => intval($payment->transaction_reference), - 'refund_reason' => 'Refund issued by merchant.', - 'amount' => $amount, - )); - $payment->recordRefund($amount); - } catch (\WePayException $ex) { - if ($ex->getCode() == 4004) { - if ($amount == $payment->amount) { - try { - // This is an uncaptured transaction; try to cancel it - $wepay->request('checkout/cancel', array( - 'checkout_id' => intval($payment->transaction_reference), - 'cancel_reason' => 'Refund issued by merchant.', - )); - $payment->markVoided(); - } catch (\WePayException $ex) { - $this->error('Unknown', $ex->getMessage(), $accountGateway); - } - } else { - $this->error('Unknown', 'Partial refund not allowed for unsettled transactions.', $accountGateway); - return false; - } - } else { - $this->error('Unknown', $ex->getMessage(), $accountGateway); - } + if (!$response->isSuccessful()) { + $this->error('Unknown', $response->getMessage(), $accountGateway); + return false; } - } } else { $payment->recordRefund($amount); @@ -1215,4 +1192,27 @@ class PaymentService extends BaseService return $e->getMessage(); } } + + public function purchase($accountGateway, $details) { + $gateway = $this->createGateway($accountGateway); + + if ($accountGateway->gateway_id == GATEWAY_WEPAY) { + $details['applicationFee'] = $this->calculateApplicationFee($accountGateway, $details['amount']); + $details['feePayer'] = WEPAY_FEE_PAYER; + } + + $response = $gateway->purchase($details)->send(); + + return $response; + } + + private function calculateApplicationFee($accountGateway, $amount) { + if ($accountGateway->gateway_id = GATEWAY_WEPAY) { + $fee = WEPAY_APP_FEE_MULTIPLIER * $amount + WEPAY_APP_FEE_FIXED; + + return floor(min($fee, $amount * 0.2));// Maximum fee is 20% of the amount. + } + + return 0; + } } diff --git a/composer.json b/composer.json index 63493330869a..60ed4b259e73 100644 --- a/composer.json +++ b/composer.json @@ -63,7 +63,6 @@ "meebio/omnipay-secure-trading": "dev-master", "justinbusschau/omnipay-secpay": "~2.0", "labs7in0/omnipay-wechat": "dev-master", - "collizo4sky/omnipay-wepay": "~1.0", "laracasts/presenter": "dev-master", "jlapp/swaggervel": "master-dev", "maatwebsite/excel": "~2.0", @@ -75,9 +74,10 @@ "barracudanetworks/archivestream-php": "^1.0", "omnipay/braintree": "~2.0@dev", "gatepay/FedACHdir": "dev-master@dev", - "wepay/php-sdk": "^0.2", "websight/l5-google-cloud-storage": "^1.0", - "fzaninotto/faker": "^1.5" + "fzaninotto/faker": "^1.5", + "wepay/php-sdk": "^0.2", + "collizo4sky/omnipay-wepay": "dev-additional-calls" }, "require-dev": { "phpunit/phpunit": "~4.0", @@ -142,6 +142,10 @@ "reference": "origin/master" } } + }, + { + "type": "vcs", + "url": "https://github.com/sometechie/omnipay-wepay" } ] } diff --git a/resources/lang/en/texts.php b/resources/lang/en/texts.php index 681640e2575b..871c89e78611 100644 --- a/resources/lang/en/texts.php +++ b/resources/lang/en/texts.php @@ -1040,14 +1040,14 @@ $LANG = array( 'enable_portal_password_help'=>'Allows you to set a password for each contact. If a password is set, the contact will be required to enter a password before viewing invoices.', 'send_portal_password'=>'Generate password automatically', 'send_portal_password_help'=>'If no password is set, one will be generated and sent with the first invoice.', - + 'expired' => 'Expired', 'invalid_card_number' => 'The credit card number is not valid.', 'invalid_expiry' => 'The expiration date is not valid.', 'invalid_cvv' => 'The CVV is not valid.', 'cost' => 'Cost', 'create_invoice_for_sample' => 'Note: create your first invoice to see a preview here.', - + // User Permissions 'owner' => 'Owner', 'administrator' => 'Administrator', @@ -1065,8 +1065,8 @@ $LANG = array( 'create_all_help' => 'Allow user to create and modify records', 'view_all_help' => 'Allow user to view records they didn\'t create', 'edit_all_help' => 'Allow user to modify records they didn\'t create', - 'view_payment' => 'View Payment', - + 'view_payment' => 'View Payment', + 'january' => 'January', 'february' => 'February', 'march' => 'March', @@ -1079,7 +1079,7 @@ $LANG = array( 'october' => 'October', 'november' => 'November', 'december' => 'December', - + // Documents 'documents_header' => 'Documents:', 'email_documents_header' => 'Documents:', @@ -1092,15 +1092,17 @@ $LANG = array( 'document_email_attachment' => 'Attach Documents', 'download_documents' => 'Download Documents (:size)', 'documents_from_expenses' => 'From Expenses:', - 'dropzone_default_message' => 'Drop files or click to upload', - 'dropzone_fallback_message' => 'Your browser does not support drag\'n\'drop file uploads.', - 'dropzone_fallback_text' => 'Please use the fallback form below to upload your files like in the olden days.', - 'dropzone_file_too_big' => 'File is too big ({{filesize}}MiB). Max filesize: {{maxFilesize}}MiB.', - 'dropzone_invalid_file_type' => 'You can\'t upload files of this type.', - 'dropzone_response_error' => 'Server responded with {{statusCode}} code.', - 'dropzone_cancel_upload' => 'Cancel upload', - 'dropzone_cancel_upload_confirmation' => 'Are you sure you want to cancel this upload?', - 'dropzone_remove_file' => 'Remove file', + 'dropzone' => array(// See http://www.dropzonejs.com/#config-dictDefaultMessage + 'DefaultMessage' => 'Drop files or click to upload', + 'FallbackMessage' => 'Your browser does not support drag\'n\'drop file uploads.', + 'FallbackText' => 'Please use the fallback form below to upload your files like in the olden days.', + 'FileTooBig' => 'File is too big ({{filesize}}MiB). Max filesize: {{maxFilesize}}MiB.', + 'InvalidFileType' => 'You can\'t upload files of this type.', + 'ResponseError' => 'Server responded with {{statusCode}} code.', + 'CancelUpload' => 'Cancel upload', + 'CancelUploadConfirmation' => 'Are you sure you want to cancel this upload?', + 'RemoveFile' => 'Remove file', + ), 'documents' => 'Documents', 'document_date' => 'Document Date', 'document_size' => 'Size', @@ -1109,11 +1111,11 @@ $LANG = array( 'enable_client_portal_help' => 'Show/hide the client portal.', 'enable_client_portal_dashboard' => 'Dashboard', 'enable_client_portal_dashboard_help' => 'Show/hide the dashboard page in the client portal.', - + // Plans 'account_management' => 'Account Management', 'plan_status' => 'Plan Status', - + 'plan_upgrade' => 'Upgrade', 'plan_change' => 'Change Plan', 'pending_change_to' => 'Changes To', @@ -1143,9 +1145,9 @@ $LANG = array( 'plan_paid' => 'Term Started', 'plan_started' => 'Plan Started', 'plan_expires' => 'Plan Expires', - + 'white_label_button' => 'White Label', - + 'pro_plan_year_description' => 'One year enrollment in the Invoice Ninja Pro Plan.', 'pro_plan_month_description' => 'One month enrollment in the Invoice Ninja Pro Plan.', 'enterprise_plan_product' => 'Enterprise Plan', @@ -1165,8 +1167,8 @@ $LANG = array( 'add_users_not_supported' => 'Upgrade to the Enterprise plan to add additional users to your account.', 'enterprise_plan_features' => 'The Enterprise plan adds support for multiple users and file attachments.', 'return_to_app' => 'Return to app', - - + + // Payment updates 'refund_payment' => 'Refund Payment', 'refund_max' => 'Max:', @@ -1183,7 +1185,7 @@ $LANG = array( 'activity_39' => ':user cancelled a :payment_amount payment (:payment)', 'activity_40' => ':user refunded :adjustment of a :payment_amount payment (:payment)', 'card_expiration' => 'Exp: :expires', - + 'card_creditcardother' => 'Unknown', 'card_americanexpress' => 'American Express', 'card_carteblanche' => 'Carte Blanche', @@ -1235,7 +1237,7 @@ $LANG = array( 'remove' => 'Remove', 'payment_method_removed' => 'Removed payment method.', 'bank_account_verification_help' => 'We have made two deposits into your account with the description "VERIFICATION". These deposits will take 1-2 business days to appear on your statement. Please enter the amounts below.', - 'bank_account_verification_next_steps' => 'We have made two deposits into your account with the description "VERIFICATION". These deposits will take 1-2 business days to appear on your statement. + 'bank_account_verification_next_steps' => 'We have made two deposits into your account with the description "VERIFICATION". These deposits will take 1-2 business days to appear on your statement. Once you have the amounts, come back to this payment methods page and click "Complete Verification" next to the account.', 'unknown_bank' => 'Unknown Bank', 'ach_verification_delay_help' => 'You will be able to use the account after completing verification. Verification usually takes 1-2 business days.', @@ -1277,11 +1279,7 @@ $LANG = array( 'no_payment_method_specified' => 'No payment method specified', - 'chart_type' => 'Chart Type', - 'format' => 'Format', - 'import_ofx' => 'Import OFX', - 'ofx_file' => 'OFX File', - 'ofx_parse_failed' => 'Failed to parse OFX file', + // WePay 'wepay' => 'WePay', @@ -1301,9 +1299,12 @@ $LANG = array( 'switch' => 'Switch', 'restore_account_gateway' => 'Restore Gateway', 'restored_account_gateway' => 'Successfully restored gateway', - + 'united_states' => 'United States', + 'canada' => 'Canada', + 'accept_debit_cards' => 'Accept Debit Cards', + 'debit_cards' => 'Debit Cards', ); return $LANG; -?>. +?>. \ No newline at end of file diff --git a/resources/views/accounts/partials/account_gateway_wepay.blade.php b/resources/views/accounts/partials/account_gateway_wepay.blade.php index 803fc37ed103..a5ccd7f01abc 100644 --- a/resources/views/accounts/partials/account_gateway_wepay.blade.php +++ b/resources/views/accounts/partials/account_gateway_wepay.blade.php @@ -5,8 +5,12 @@ 'description' => 'required', 'company_name' => 'required', 'tos_agree' => 'required', + 'country' => 'required', ))->addClass('warn-on-exit') !!} {!! Former::populateField('company_name', $account->getDisplayName()) !!} +@if($account->country) + {!! Former::populateField('country', $account->country->iso_3166_2) !!} +@endif {!! Former::populateField('first_name', $user->first_name) !!} {!! Former::populateField('last_name', $user->last_name) !!} {!! Former::populateField('email', $user->email) !!} @@ -23,25 +27,34 @@ {!! Former::text('email') !!} {!! Former::text('company_name')->help('wepay_company_name_help')->maxlength(255) !!} {!! Former::text('description')->help('wepay_description_help') !!} + @if (WEPAY_ENABLE_CANADA) +