Skip to content

Commit

Permalink
Adding Stripe link payments
Browse files Browse the repository at this point in the history
  • Loading branch information
Fernando González committed Oct 29, 2022
1 parent d8634dc commit 371678a
Show file tree
Hide file tree
Showing 58 changed files with 1,075 additions and 90 deletions.
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,7 @@ The application is designed to be flexible enough so that it can handle any ente
* Self hosted installation.
* Translated user interface.
* User community support.
* Service payment by [Stripe Payment links](https://stripe.com/en-gb-es/payments/payment-links)

## Setup

Expand Down
16 changes: 14 additions & 2 deletions application/config/config.php
Original file line number Diff line number Diff line change
Expand Up @@ -458,12 +458,24 @@
| Rate Limiting
|--------------------------------------------------------------------------
|
| Toggle the rate limiting feature in your application. Using rate limiting
| will control the number of requests a client can sent to the app.
| Toggle the rate limiting feature in your application. Using rate limiting
| will control the number of requests a client can sent to the app.
|
*/
$config['rate_limiting'] = TRUE;


/*
|--------------------------------------------------------------------------
| Stripe Payment Configuration
|--------------------------------------------------------------------------
|
| Declare some of the global config values of the Stripe Payments
|
*/

$config['stripe_payment_feature'] = Config::STRIPE_PAYMENT_FEATURE;
$config['stripe_api_key'] = Config::STRIPE_API_KEY;

/* End of file config.php */
/* Location: ./application/config/config.php */
8 changes: 6 additions & 2 deletions application/controllers/Booking.php
Original file line number Diff line number Diff line change
Expand Up @@ -93,7 +93,7 @@ public function index()

foreach ($available_providers as &$available_provider)
{
// Only expose the required provider data.
// Only expose the required provider data.

$this->providers_model->only($available_provider, [
'id',
Expand Down Expand Up @@ -170,7 +170,7 @@ public function index()
return;
}

// Make sure the appointment can still be rescheduled.
// Make sure the appointment can still be rescheduled.

$start_datetime = strtotime($results[0]['start_datetime']);

Expand Down Expand Up @@ -203,6 +203,7 @@ public function index()
$provider = $this->providers_model->find($appointment['id_users_provider']);
$customer = $this->customers_model->find($appointment['id_users_customer']);
$customer_token = md5(uniqid(mt_rand(), TRUE));
$is_paid = $appointment['is_paid'];

// Cache the token for 10 minutes.
$this->cache->save('customer-token-' . $customer_token, $customer['id'], 600);
Expand All @@ -214,6 +215,7 @@ public function index()
$appointment = NULL;
$provider = NULL;
$customer = NULL;
$is_paid = 0;
}

script_vars([
Expand Down Expand Up @@ -271,9 +273,11 @@ public function index()
'grouped_timezones' => $grouped_timezones,
'manage_mode' => $manage_mode,
'customer_token' => $customer_token,
'is_paid' => $is_paid == 1,
'appointment_data' => $appointment,
'provider_data' => $provider,
'customer_data' => $customer,
'company_email' => setting('company_email'),
]);

$this->load->view('pages/booking');
Expand Down
16 changes: 9 additions & 7 deletions application/controllers/Calendar.php
Original file line number Diff line number Diff line change
Expand Up @@ -134,6 +134,7 @@ public function index(string $appointment_hash = '')
'secretary_providers' => $secretary_providers,
'edit_appointment' => $edit_appointment,
'customers' => $this->customers_model->get(NULL, 50, NULL, 'update_datetime DESC'),
'stripe_payment_feature' => config('stripe_payment_feature'),
]);

html_vars([
Expand Down Expand Up @@ -233,7 +234,7 @@ public function save_appointment()
}

// If the appointment does not contain the customer record id, then it means that is going to be
// inserted.
// inserted.
if ( ! isset($appointment['id_users_customer']))
{
$appointment['id_users_customer'] = $customer['id'] ?? $customer_data['id'];
Expand All @@ -256,6 +257,7 @@ public function save_appointment()
'id_users_provider',
'id_users_customer',
'id_services',
'is_paid',
]);

$appointment['id'] = $this->appointments_model->save($appointment);
Expand Down Expand Up @@ -629,9 +631,9 @@ public function get_calendar_appointments()
$end_date = $this->db->escape(date('Y-m-d', strtotime(request('end_date') . ' +1 day')));

$where_clause = $where_id . ' = ' . $record_id . '
AND ((start_datetime > ' . $start_date . ' AND start_datetime < ' . $end_date . ')
or (end_datetime > ' . $start_date . ' AND end_datetime < ' . $end_date . ')
or (start_datetime <= ' . $start_date . ' AND end_datetime >= ' . $end_date . '))
AND ((start_datetime > ' . $start_date . ' AND start_datetime < ' . $end_date . ')
or (end_datetime > ' . $start_date . ' AND end_datetime < ' . $end_date . ')
or (start_datetime <= ' . $start_date . ' AND end_datetime >= ' . $end_date . '))
AND is_unavailability = 0
';

Expand All @@ -650,9 +652,9 @@ public function get_calendar_appointments()
if ($filter_type == FILTER_TYPE_PROVIDER)
{
$where_clause = $where_id . ' = ' . $record_id . '
AND ((start_datetime > ' . $start_date . ' AND start_datetime < ' . $end_date . ')
or (end_datetime > ' . $start_date . ' AND end_datetime < ' . $end_date . ')
or (start_datetime <= ' . $start_date . ' AND end_datetime >= ' . $end_date . '))
AND ((start_datetime > ' . $start_date . ' AND start_datetime < ' . $end_date . ')
or (end_datetime > ' . $start_date . ' AND end_datetime < ' . $end_date . ')
or (start_datetime <= ' . $start_date . ' AND end_datetime >= ' . $end_date . '))
AND is_unavailability = 1
';

Expand Down
245 changes: 245 additions & 0 deletions application/controllers/Payment.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,245 @@
<?php defined('BASEPATH') or exit('No direct script access allowed');

/* ----------------------------------------------------------------------------
* Easy!Appointments - Online Appointment Scheduler
*
* @package EasyAppointments
* @author F.González <fernando@tuta.io>
* @copyright Copyright (c) Alex Tselegidis
* @license https://opensource.org/licenses/GPL-3.0 - GPLv3
* @link https://easyappointments.org
* @since v1.0.0
* ---------------------------------------------------------------------------- */

/**
* Payment confirmation controller.
*
* Handles the confirmation of a payment.
*
*
* @package Controllers
*/
class Payment extends EA_Controller {
/**
* Booking constructor.
*/
public function __construct()
{
parent::__construct();

$this->load->model('appointments_model');
$this->load->model('providers_model');
$this->load->model('admins_model');
$this->load->model('secretaries_model');
$this->load->model('categories_model');
$this->load->model('services_model');
$this->load->model('customers_model');
$this->load->model('settings_model');
$this->load->model('consents_model');

$this->load->library('timezones');
$this->load->library('synchronization');
$this->load->library('notifications');
$this->load->library('availability');
$this->load->library('webhooks_client');

$this->load->driver('cache', ['adapter' => 'file']);
}

/**
* Render the payment confirmation page.
*/
public function index()
{
if ( ! is_app_installed())
{
redirect('installation');

return;
}

$appointment = html_vars('appointment');

if (empty($appointment)) {
abort(404, "Forbidden");
} else {
$manage_mode = TRUE;
$company_name = setting('company_name');
$company_logo = setting('company_logo');
$company_color = setting('company_color');
$google_analytics_code = setting('google_analytics_code');
$matomo_analytics_url = setting('matomo_analytics_url');
$date_format = setting('date_format');
$time_format = setting('time_format');

$display_first_name = setting('display_first_name');
$require_first_name = setting('require_first_name');
$display_last_name = setting('display_last_name');
$require_last_name = setting('require_last_name');
$display_email = setting('display_email');
$require_email = setting('require_email');
$display_phone_number = setting('display_phone_number');
$require_phone_number = setting('require_phone_number');
$display_address = setting('display_address');
$require_address = setting('require_address');
$display_city = setting('display_city');
$require_city = setting('require_city');
$display_zip_code = setting('display_zip_code');
$require_zip_code = setting('require_zip_code');
$display_notes = setting('display_notes');
$require_notes = setting('require_notes');
$display_cookie_notice = setting('display_cookie_notice');
$cookie_notice_content = setting('cookie_notice_content');
$display_terms_and_conditions = setting('display_terms_and_conditions');
$terms_and_conditions_content = setting('terms_and_conditions_content');
$display_privacy_policy = setting('display_privacy_policy');
$privacy_policy_content = setting('privacy_policy_content');

$theme = request('theme', setting('theme', 'default'));
if (empty($theme) || ! file_exists(__DIR__ . '/../../assets/css/themes/' . $theme . '.min.css'))
{
$theme = 'default';
}

$timezones = $this->timezones->to_array();
$grouped_timezones = $this->timezones->to_grouped_array();
$provider = $this->providers_model->find($appointment['id_users_provider']);
$customer = $this->customers_model->find($appointment['id_users_customer']);

script_vars([
'date_format' => $date_format,
'time_format' => $time_format,
'display_cookie_notice' => $display_cookie_notice,
'display_any_provider' => setting('display_any_provider'),
]);

html_vars([
'theme' => $theme,
'company_name' => $company_name,
'company_logo' => $company_logo,
'company_color' => $company_color === '#ffffff' ? '' : $company_color,
'date_format' => $date_format,
'time_format' => $time_format,
'display_first_name' => $display_first_name,
'display_last_name' => $display_last_name,
'display_email' => $display_email,
'display_phone_number' => $display_phone_number,
'display_address' => $display_address,
'display_city' => $display_city,
'display_zip_code' => $display_zip_code,
'display_notes' => $display_notes,
'google_analytics_code' => $google_analytics_code,
'matomo_analytics_url' => $matomo_analytics_url,
'timezones' => $timezones,
'grouped_timezones' => $grouped_timezones,
'appointment' => $appointment,
'provider' => $provider,
'customer' => $customer,
]);

$this->load->view('pages/payment');
}
}

/**
* Validates Stripe payment and render confirmation screen for the appointment.
*
* This method sets a flag as paid for an appointment and call the "index" callback
* to handle the page rendering.
*
* @param string $checkout_session_id Stripe session id.
*/
public function confirm(string $checkout_session_id)
{
try
{
$stripe_api_key = config('stripe_api_key');

$stripe = new \Stripe\StripeClient($stripe_api_key);

$session = $stripe->checkout->sessions->retrieve($checkout_session_id);

$appointment_hash = $session->client_reference_id;
$payment_intent = $session->payment_intent;

$appointment = $this->set_paid($appointment_hash, $payment_intent);

html_vars(['appointment' => $appointment]);

$this->index();
}
catch (Throwable $e)
{
error_log( $e );
abort(500, 'Internal server error');
}
}

/**
* Sets a paid flag and paid intent for an appointment to track paid bookings.
*/
private function set_paid($appointment_hash, $payment_intent)
{
try
{
$manage_mode = TRUE;

$occurrences = $this->appointments_model->get(['hash' => $appointment_hash]);

if (empty($occurrences))
{
abort(404, 'Not Found');
}

$appointment = $occurrences[0];

$provider = $this->providers_model->find($appointment['id_users_provider']);

$customer = $this->customers_model->find($appointment['id_users_customer']);

$service = $this->services_model->find($appointment['id_services']);


$appointment['is_paid'] = 1;
$appointment['payment_intent'] = $payment_intent;
$this->appointments_model->only($appointment, [
'id',
'start_datetime',
'end_datetime',
'location',
'notes',
'color',
'is_unavailability',
'id_users_provider',
'id_users_customer',
'id_services',
'is_paid',
'payment_intent',
]);
$appointment_id = $this->appointments_model->save($appointment);
$appointment = $this->appointments_model->find($appointment_id);

$settings = [
'company_name' => setting('company_name'),
'company_link' => setting('company_link'),
'company_email' => setting('company_email'),
'date_format' => setting('date_format'),
'time_format' => setting('time_format')
];

$this->synchronization->sync_appointment_saved($appointment, $service, $provider, $customer, $settings, $manage_mode);

$this->notifications->notify_appointment_saved($appointment, $service, $provider, $customer, $settings, $manage_mode);

$this->webhooks_client->trigger(WEBHOOK_APPOINTMENT_SAVE, $appointment);

return $appointment;
}
catch (Throwable $e)
{
error_log( $e );
abort(500, 'Internal server error');
}
}

}
8 changes: 8 additions & 0 deletions application/language/arabic/translations_lang.php
Original file line number Diff line number Diff line change
Expand Up @@ -408,4 +408,12 @@
$lang['status'] = 'Status';
$lang['appointment_status_options'] = 'Appointment Status Options';
$lang['appointment_status_options_info'] = 'Define a list of available appointment status options that can be used in the the calendar page (the first one will automatically become the default value).';
$lang['service_payment_link'] = 'Payment link';
$lang['service_payment_link_description'] = 'You can include the following variables: {$appointment_hash}, {$customer_email}. In order to confirm the payment use https://<YOUR_SERVER>/index.php/payment/confirm/{CHECKOUT_SESSION_ID} url as confirmation page in Stripe';
$lang['appointment_payment_title'] = 'Payment details';
$lang['appointment_payment_text'] = 'Click on the following link to proceed with payment';
$lang['appointment_paymentPaid_text'] = 'Payment has been successfully completed';
$lang['payment_intent'] = 'Stripe Payment Intent';
$lang['service_paid_warning'] = 'It is not possible to change an already paid service. Please contact us at {$mail_link} for more information.';
$lang['is_paid'] = 'Paid';
// End

0 comments on commit 371678a

Please sign in to comment.