diff --git a/.env.ci b/.env.ci index 486d20cfd0f1..1fde559c0b81 100644 --- a/.env.ci +++ b/.env.ci @@ -19,4 +19,5 @@ DB_HOST=127.0.0.1 NINJA_ENVIRONMENT=hosted COMPOSER_AUTH='{"github-oauth": {"github.com": "${{ secrets.GITHUB_TOKEN }}"}}' TRAVIS=true -API_SECRET=superdoopersecrethere \ No newline at end of file +API_SECRET=superdoopersecrethere +PHANTOMJS_PDF_GENERATION=false diff --git a/CHANGELOG.md b/CHANGELOG.md index c5047dcfcea6..aa55822c0b68 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,8 +7,8 @@ ### Fixed: - Fixes for counters where patterns without {$counter} could causes endless recursion. - Fixes for surcharge tax displayed amount on PDF. - -### Removed: +- Fixes for custom designs not rendering the custom template +- Fixes for missing bulk actions on Subscriptions ## v5.1.43 diff --git a/app/Exceptions/FilePermissionsFailure.php b/app/Exceptions/FilePermissionsFailure.php new file mode 100644 index 000000000000..760a3b2d7500 --- /dev/null +++ b/app/Exceptions/FilePermissionsFailure.php @@ -0,0 +1,10 @@ +expectsJson()) { return response()->json(['message'=>$exception->getMessage()], 400); + }elseif($exception instanceof InternalPDFFailure && $request->expectsJson()){ + return response()->json(['message' => $exception->getMessage()], 500); + }elseif($exception instanceof PhantomPDFFailure && $request->expectsJson()){ + return response()->json(['message' => $exception->getMessage()], 500); + }elseif($exception instanceof FilePermissionsFailure) { + return response()->json(['message' => $exception->getMessage()], 500); } elseif ($exception instanceof ThrottleRequestsException && $request->expectsJson()) { return response()->json(['message'=>'Too many requests'], 429); } elseif ($exception instanceof FatalThrowableError && $request->expectsJson()) { @@ -152,8 +161,7 @@ class Handler extends ExceptionHandler } elseif ($exception instanceof MethodNotAllowedHttpException && $request->expectsJson()) { return response()->json(['message'=>'Method not support for this route'], 404); } elseif ($exception instanceof ValidationException && $request->expectsJson()) { - info(print_r($exception->validator->getMessageBag(), 1)); - + nlog($exception->validator->getMessageBag()); return response()->json(['message' => 'The given data was invalid.', 'errors' => $exception->validator->getMessageBag()], 422); } elseif ($exception instanceof RelationNotFoundException && $request->expectsJson()) { return response()->json(['message' => $exception->getMessage()], 400); @@ -161,9 +169,7 @@ class Handler extends ExceptionHandler 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); } diff --git a/app/Exceptions/InternalPDFFailure.php b/app/Exceptions/InternalPDFFailure.php new file mode 100644 index 000000000000..ce85ad3c7ebb --- /dev/null +++ b/app/Exceptions/InternalPDFFailure.php @@ -0,0 +1,10 @@ +json(['message' => ctrans('texts.self_update_not_available')], 403); } + if(!$this->testWritable()) + throw new FilePermissionsFailure('Cannot update system because files are not writable!'); + // Check if new version is available if($updater->source()->isNewVersionAvailable()) { @@ -90,6 +94,19 @@ class SelfUpdateController extends BaseController } + private function testWritable() + { + $directoryIterator = new \RecursiveDirectoryIterator(base_path()); + + foreach (new \RecursiveIteratorIterator($directoryIterator) as $file) { + if ($file->isFile() && ! $file->isWritable()) { + return false; + } + } + + return true; + } + public function checkVersion() { return trim(file_get_contents(config('ninja.version_url'))); diff --git a/app/Jobs/Entity/CreateEntityPdf.php b/app/Jobs/Entity/CreateEntityPdf.php index edaa6776ea8b..79a479ac9840 100644 --- a/app/Jobs/Entity/CreateEntityPdf.php +++ b/app/Jobs/Entity/CreateEntityPdf.php @@ -12,6 +12,7 @@ namespace App\Jobs\Entity; +use App\Exceptions\FilePermissionsFailure; use App\Models\Account; use App\Models\Credit; use App\Models\CreditInvitation; @@ -168,6 +169,7 @@ class CreateEntityPdf implements ShouldQueue else { $pdf = $this->makePdf(null, null, $maker->getCompiledHTML(true)); } + } catch (\Exception $e) { nlog(print_r($e->getMessage(), 1)); } @@ -176,8 +178,20 @@ class CreateEntityPdf implements ShouldQueue info($maker->getCompiledHTML()); } + if ($pdf) { - Storage::disk($this->disk)->put($file_path, $pdf); + + try{ + + Storage::disk($this->disk)->put($file_path, $pdf); + + } + catch(\Exception $e) + { + + throw new FilePermissionsFailure('Could not write the PDF, permission issues!'); + + } } return $file_path; diff --git a/app/PaymentDrivers/Stripe/Connect/Account.php b/app/PaymentDrivers/Stripe/Connect/Account.php new file mode 100644 index 000000000000..36432c047cdf --- /dev/null +++ b/app/PaymentDrivers/Stripe/Connect/Account.php @@ -0,0 +1,242 @@ +accounts->create([ +// 'type' => 'custom', +// 'country' => 'US', +// 'email' => 'jenny.rosen@example.com', +// 'capabilities' => [ +// 'card_payments' => ['requested' => true], +// 'transfers' => ['requested' => true], +// ], +// ]); +/// + + +//response + +/** + * { + "id": "acct_1032D82eZvKYlo2C", + "object": "account", + "business_profile": { + "mcc": null, + "name": "Stripe.com", + "product_description": null, + "support_address": null, + "support_email": null, + "support_phone": null, + "support_url": null, + "url": null + }, + "capabilities": { + "card_payments": "active", + "transfers": "active" + }, + "charges_enabled": false, + "country": "US", + "default_currency": "usd", + "details_submitted": false, + "email": "site@stripe.com", + "metadata": {}, + "payouts_enabled": false, + "requirements": { + "current_deadline": null, + "currently_due": [ + "business_profile.product_description", + "business_profile.support_phone", + "business_profile.url", + "external_account", + "tos_acceptance.date", + "tos_acceptance.ip" + ], + "disabled_reason": "requirements.past_due", + "errors": [], + "eventually_due": [ + "business_profile.product_description", + "business_profile.support_phone", + "business_profile.url", + "external_account", + "tos_acceptance.date", + "tos_acceptance.ip" + ], + "past_due": [], + "pending_verification": [] + }, + "settings": { + "bacs_debit_payments": {}, + "branding": { + "icon": null, + "logo": null, + "primary_color": null, + "secondary_color": null + }, + "card_issuing": { + "tos_acceptance": { + "date": null, + "ip": null + } + }, + "card_payments": { + "decline_on": { + "avs_failure": true, + "cvc_failure": false + }, + "statement_descriptor_prefix": null + }, + "dashboard": { + "display_name": "Stripe.com", + "timezone": "US/Pacific" + }, + "payments": { + "statement_descriptor": null, + "statement_descriptor_kana": null, + "statement_descriptor_kanji": null + }, + "payouts": { + "debit_negative_balances": true, + "schedule": { + "delay_days": 7, + "interval": "daily" + }, + "statement_descriptor": null + }, + "sepa_debit_payments": {} + }, + "type": "standard" +} + + */ + + + +//then create the account link + +// https://stripe.com/docs/api/account_links/create?lang=php +} \ No newline at end of file diff --git a/app/PaymentDrivers/StripeConnectPaymentDriver.php b/app/PaymentDrivers/StripeConnectPaymentDriver.php new file mode 100644 index 000000000000..6162e1556fbe --- /dev/null +++ b/app/PaymentDrivers/StripeConnectPaymentDriver.php @@ -0,0 +1,436 @@ + CreditCard::class, + GatewayType::BANK_TRANSFER => ACH::class, + GatewayType::ALIPAY => Alipay::class, + GatewayType::SOFORT => SOFORT::class, + GatewayType::APPLE_PAY => 1, // TODO + GatewayType::SEPA => 1, // TODO + ]; + + const SYSTEM_LOG_TYPE = SystemLog::TYPE_STRIPE; + + /** + * Initializes the Stripe API. + * @return void + */ + public function init(): void + { + $this->stripe = new StripeClient( + $this->company_gateway->getConfigField('apiKey') + ); + + Stripe::setApiKey($this->company_gateway->getConfigField('apiKey')); + } + + 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, + GatewayType::CRYPTO, +// GatewayType::SEPA, // TODO: Missing implementation +// GatewayType::APPLE_PAY, // TODO:: Missing implementation + ]; + + if ($this->client + && isset($this->client->country) + && in_array($this->client->country->iso_3166_3, ['AUT', 'BEL', 'DEU', 'ITA', 'NLD', 'ESP'])) { + $types[] = GatewayType::SOFORT; + } + + if ($this->client + && isset($this->client->country) + && in_array($this->client->country->iso_3166_3, ['USA'])) { + $types[] = GatewayType::BANK_TRANSFER; + } + + if ($this->client + && isset($this->client->country) + && in_array($this->client->country->iso_3166_3, ['AUS', 'DNK', 'DEU', 'ITA', 'LUX', 'NOR', 'SVN', 'GBR', 'AUT', 'EST', 'GRC', 'JPN', 'MYS', 'PRT', 'ESP', 'USA', 'BEL', 'FIN', 'HKG', 'LVA', 'NLD', 'SGP', 'SWE', 'CAN', 'FRA', 'IRL', 'LTU', 'NZL', 'SVK', 'CHE'])) { + $types[] = GatewayType::ALIPAY; + } + + return $types; + } + + public function viewForType($gateway_type_id) + { + switch ($gateway_type_id) { + case GatewayType::CREDIT_CARD: + return 'gateways.stripe.credit_card'; + break; + case GatewayType::SOFORT: + return 'gateways.stripe.sofort'; + break; + case GatewayType::BANK_TRANSFER: + return 'gateways.stripe.ach'; + break; + case GatewayType::SEPA: + return 'gateways.stripe.sepa'; + break; + case GatewayType::CRYPTO: + case GatewayType::ALIPAY: + case GatewayType::APPLE_PAY: + return 'gateways.stripe.other'; + break; + + default: + break; + } + } + + public function getClientRequiredFields(): array + { + $fields = [ + ['name' => 'client_postal_code', 'label' => ctrans('texts.postal_code'), 'type' => 'text', 'validation' => 'required'], + ]; + + if ($this->company_gateway->require_client_name) { + $fields[] = ['name' => 'client_name', 'label' => ctrans('texts.client_name'), 'type' => 'text', 'validation' => 'required']; + } + + if ($this->company_gateway->require_client_phone) { + $fields[] = ['name' => 'client_phone', 'label' => ctrans('texts.client_phone'), 'type' => 'tel', 'validation' => 'required']; + } + + if ($this->company_gateway->require_contact_name) { + $fields[] = ['name' => 'contact_first_name', 'label' => ctrans('texts.first_name'), 'type' => 'text', 'validation' => 'required']; + $fields[] = ['name' => 'contact_last_name', 'label' => ctrans('texts.last_name'), 'type' => 'text', 'validation' => 'required']; + } + + if ($this->company_gateway->require_contact_email) { + $fields[] = ['name' => 'contact_email', 'label' => ctrans('texts.email'), 'type' => 'text', 'validation' => 'required,email:rfc']; + } + + if ($this->company_gateway->require_billing_address) { + $fields[] = ['name' => 'client_address_line_1', 'label' => ctrans('texts.address1'), 'type' => 'text', 'validation' => 'required']; + $fields[] = ['name' => 'client_address_line_2', 'label' => ctrans('texts.address2'), 'type' => 'text', 'validation' => 'required']; + $fields[] = ['name' => 'client_city', 'label' => ctrans('texts.city'), 'type' => 'text', 'validation' => 'required']; + $fields[] = ['name' => 'client_state', 'label' => ctrans('texts.state'), 'type' => 'text', 'validation' => 'required']; + $fields[] = ['name' => 'client_country_id', 'label' => ctrans('texts.country'), 'type' => 'text', 'validation' => 'required']; + } + + if ($this->company_gateway->require_shipping_address) { + $fields[] = ['name' => 'client_shipping_address_line_1', 'label' => ctrans('texts.shipping_address1'), 'type' => 'text', 'validation' => 'required']; + $fields[] = ['name' => 'client_shipping_address_line_2', 'label' => ctrans('texts.shipping_address2'), 'type' => 'text', 'validation' => 'required']; + $fields[] = ['name' => 'client_shipping_city', 'label' => ctrans('texts.shipping_city'), 'type' => 'text', 'validation' => 'required']; + $fields[] = ['name' => 'client_shipping_state', 'label' => ctrans('texts.shipping_state'), 'type' => 'text', 'validation' => 'required']; + $fields[] = ['name' => 'client_shipping_postal_code', 'label' => ctrans('texts.shipping_postal_code'), 'type' => 'text', 'validation' => 'required']; + $fields[] = ['name' => 'client_shipping_country_id', 'label' => ctrans('texts.shipping_country'), 'type' => 'text', 'validation' => 'required']; + } + + return $fields; + } + + /** + * Proxy method to pass the data into payment method authorizeView(). + * + * @param array $data + * @return \Illuminate\Http\RedirectResponse|mixed + */ + public function authorizeView(array $data) + { + return $this->payment_method->authorizeView($data); + } + + /** + * Processes the gateway response for credit card authorization. + * + * @param \Illuminate\Http\Request $request + * @return \Illuminate\Http\RedirectResponse|mixed + */ + public function authorizeResponse($request) + { + return $this->payment_method->authorizeResponse($request); + } + + /** + * Process the payment with gateway. + * + * @param array $data + * @return \Illuminate\Http\RedirectResponse|mixed + */ + public function processPaymentView(array $data) + { + return $this->payment_method->paymentView($data); + } + + public function processPaymentResponse($request) + { + return $this->payment_method->paymentResponse($request); + } + + /** + * Creates a new String Payment Intent. + * + * @param array $data The data array to be passed to Stripe + * @return PaymentIntent The Stripe payment intent object + * @throws ApiErrorException + */ + public function createPaymentIntent($data): ?PaymentIntent + { + $this->init(); + + return PaymentIntent::create($data); + } + + /** + * Returns a setup intent that allows the user + * to enter card details without initiating a transaction. + * + * @return SetupIntent + * @throws ApiErrorException + */ + public function getSetupIntent(): SetupIntent + { + $this->init(); + + return SetupIntent::create(); + } + + /** + * Returns the Stripe publishable key. + * @return null|string The stripe publishable key + */ + public function getPublishableKey(): ?string + { + return $this->company_gateway->getPublishableKey(); + } + + /** + * Finds or creates a Stripe Customer object. + * + * @return null|Customer A Stripe customer object + * @throws \Laracasts\Presenter\Exceptions\PresenterException + * @throws ApiErrorException + */ + public function findOrCreateCustomer(): ?Customer + { + $customer = null; + + $this->init(); + + $client_gateway_token = ClientGatewayToken::whereClientId($this->client->id)->whereCompanyGatewayId($this->company_gateway->id)->first(); + + if ($client_gateway_token && $client_gateway_token->gateway_customer_reference) { + $customer = Customer::retrieve($client_gateway_token->gateway_customer_reference); + } else { + $data['name'] = $this->client->present()->name(); + $data['phone'] = $this->client->present()->phone(); + + if (filter_var($this->client->present()->email(), FILTER_VALIDATE_EMAIL)) { + $data['email'] = $this->client->present()->email(); + } + + $customer = Customer::create($data); + } + + if (!$customer) { + throw new Exception('Unable to create gateway customer'); + } + + return $customer; + } + + public function refund(Payment $payment, $amount, $return_client_response = false) + { + $this->init(); + + /** Response from Stripe SDK/API. */ + $response = null; + + try { + $response = $this->stripe + ->refunds + ->create(['charge' => $payment->transaction_reference, 'amount' => $this->convertToStripeAmount($amount, $this->client->currency()->precision)]); + + if ($response->status == $response::STATUS_SUCCEEDED) { + SystemLogger::dispatch(['server_response' => $response, 'data' => request()->all(),], SystemLog::CATEGORY_GATEWAY_RESPONSE, SystemLog::EVENT_GATEWAY_SUCCESS, SystemLog::TYPE_STRIPE, $this->client); + + return [ + 'transaction_reference' => $response->charge, + 'transaction_response' => json_encode($response), + 'success' => $response->status == $response::STATUS_SUCCEEDED ? true : false, + 'description' => $response->metadata, + 'code' => $response, + ]; + } + + SystemLogger::dispatch(['server_response' => $response, 'data' => request()->all(),], SystemLog::CATEGORY_GATEWAY_RESPONSE, SystemLog::EVENT_GATEWAY_FAILURE, SystemLog::TYPE_STRIPE, $this->client); + + return [ + 'transaction_reference' => null, + 'transaction_response' => json_encode($response), + 'success' => false, + 'description' => $response->failure_reason, + 'code' => 422, + ]; + } catch (Exception $e) { + SystemLogger::dispatch(['server_response' => $response, 'data' => request()->all(),], SystemLog::CATEGORY_GATEWAY_RESPONSE, SystemLog::EVENT_GATEWAY_FAILURE, SystemLog::TYPE_STRIPE, $this->client); + + nlog($e->getMessage()); + + return [ + 'transaction_reference' => null, + 'transaction_response' => json_encode($response), + 'success' => false, + 'description' => $e->getMessage(), + 'code' => 422, + ]; + } + } + + public function verificationView(ClientGatewayToken $payment_method) + { + return $this->payment_method->verificationView($payment_method); + } + + public function processVerification(Request $request, ClientGatewayToken $payment_method) + { + return $this->payment_method->processVerification($request, $payment_method); + } + + public function processWebhookRequest(PaymentWebhookRequest $request, Payment $payment) + { + if ($request->type == 'source.chargeable') { + $payment->status_id = Payment::STATUS_COMPLETED; + $payment->save(); + } + + return response([], 200); + } + + public function tokenBilling(ClientGatewayToken $cgt, PaymentHash $payment_hash) + { + return (new Charge($this))->tokenBilling($cgt, $payment_hash); + } + + /** + * Attach Stripe payment method to Stripe client. + * + * @param string $payment_method + * @param mixed $customer + * + * @return void + */ + public function attach(string $payment_method, $customer): void + { + try { + $stripe_payment_method = $this->getStripePaymentMethod($payment_method); + $stripe_payment_method->attach(['customer' => $customer->id]); + } catch (ApiErrorException | Exception $e) { + $this->processInternallyFailedPayment($this, $e); + } + } + + /** + * Detach payment method from the Stripe. + * https://stripe.com/docs/api/payment_methods/detach + * + * @param ClientGatewayToken $token + * @return void + */ + public function detach(ClientGatewayToken $token) + { + $stripe = new StripeClient( + $this->company_gateway->getConfigField('apiKey') + ); + + try { + $stripe->paymentMethods->detach($token->token); + } catch (Exception $e) { + SystemLogger::dispatch([ + 'server_response' => $e->getMessage(), 'data' => request()->all(), + ], SystemLog::CATEGORY_GATEWAY_RESPONSE, SystemLog::EVENT_GATEWAY_FAILURE, SystemLog::TYPE_STRIPE, $this->client); + } + } + + public function getCompanyGatewayId(): int + { + return $this->company_gateway->id; + } + + /** + * Retrieve payment method from Stripe. + * + * @param string $source + * + * @return PaymentMethod|void + */ + public function getStripePaymentMethod(string $source) + { + try { + return PaymentMethod::retrieve($source); + } catch (ApiErrorException | Exception $e) { + return $this->processInternallyFailedPayment($this, $e); + } + } +} diff --git a/app/Services/Subscription/SubscriptionService.php b/app/Services/Subscription/SubscriptionService.php index 494e53d2b141..f372bcd7c07c 100644 --- a/app/Services/Subscription/SubscriptionService.php +++ b/app/Services/Subscription/SubscriptionService.php @@ -183,7 +183,7 @@ class SubscriptionService return redirect('/client/recurring_invoices/'.$recurring_invoice->hashed_id); } - public function calculateUpgradePrice(RecurringInvoice $recurring_invoice, Subscription $target) + public function calculateUpgradePrice(RecurringInvoice $recurring_invoice, Subscription $target) :?float { //calculate based on daily prices @@ -206,14 +206,17 @@ class SubscriptionService //user has multiple amounts outstanding return $target->price - $this->calculateProRataRefund($outstanding->first()); } - elseif ($outstanding->count > 1) { + elseif ($outstanding->count() > 1) { //user is changing plan mid frequency cycle //we cannot handle this if there are more than one invoice outstanding. + return null; } + return null; + } - private function calculateProRataRefund($invoice) + private function calculateProRataRefund($invoice) :float { //determine the start date diff --git a/app/Utils/PhantomJS/Phantom.php b/app/Utils/PhantomJS/Phantom.php index 3f2945cfbb42..a9efb57941fd 100644 --- a/app/Utils/PhantomJS/Phantom.php +++ b/app/Utils/PhantomJS/Phantom.php @@ -11,6 +11,7 @@ namespace App\Utils\PhantomJS; +use App\Exceptions\PhantomPDFFailure; use App\Jobs\Util\SystemLogger; use App\Models\CreditInvitation; use App\Models\Design; @@ -91,8 +92,6 @@ class Phantom $instance = Storage::disk(config('filesystems.default'))->put($file_path, $pdf); -// nlog($instance); -// nlog($file_path); return $file_path; } @@ -128,6 +127,8 @@ class Phantom SystemLog::TYPE_PDF_FAILURE, $invitation->contact->client ); + + throw new PhantomPDFFailure('There was an error generating the PDF with Phantom JS'); } else { diff --git a/app/Utils/Traits/Pdf/PdfMaker.php b/app/Utils/Traits/Pdf/PdfMaker.php index 95343e425703..0d26f5eff0df 100644 --- a/app/Utils/Traits/Pdf/PdfMaker.php +++ b/app/Utils/Traits/Pdf/PdfMaker.php @@ -12,6 +12,7 @@ namespace App\Utils\Traits\Pdf; +use App\Exceptions\InternalPDFFailure; use Beganovich\Snappdf\Snappdf; trait PdfMaker @@ -33,8 +34,14 @@ trait PdfMaker $pdf->setChromiumPath(config('ninja.snappdf_chromium_path')); } - return $pdf - ->setHtml($html) - ->generate(); + $generated = $pdf + ->setHtml($html) + ->generate(); + + if($generated) + return $generated; + + + throw new InternalPDFFailure('There was an issue generating the PDF locally'); } } diff --git a/database/migrations/2021_04_12_095424_stripe_connect_gateway.php b/database/migrations/2021_04_12_095424_stripe_connect_gateway.php new file mode 100644 index 000000000000..110c36582dde --- /dev/null +++ b/database/migrations/2021_04_12_095424_stripe_connect_gateway.php @@ -0,0 +1,49 @@ + 56, + 'name' => 'Stripe Connect', + 'provider' => 'StripeConnect', + 'sort_order' => 1, + 'key' => 'd14dd26a47cecc30fdd65700bfb67b34', + 'fields' => '{"apiKey":"", "publishableKey":""}' + ]; + + Gateway::create($gateway); + + if(Ninja::isNinja()) + { + Gateway::where('id', 20)->update(['visible' => 0]); + Gateway::where('id', 56)->update(['visible' => 1]); + } + + Model::guard(); + + } + + /** + * Reverse the migrations. + * + * @return void + */ + public function down() + { + // + } +}