Skip to content

Commit 4aa8970

Browse files
author
Fernando González
committed
Adding Stripe link payments
1 parent f0a9160 commit 4aa8970

File tree

62 files changed

+1179
-37
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

62 files changed

+1179
-37
lines changed

README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,7 @@ The application is designed to be flexible enough so that it can handle any ente
5353
* Self hosted installation.
5454
* Translated user interface.
5555
* User community support.
56+
* Service payment by [Stripe Payment links](https://stripe.com/en-gb-es/payments/payment-links)
5657

5758
## Setup
5859

application/config/config.php

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -466,5 +466,17 @@
466466
$config['rate_limiting'] = TRUE;
467467

468468

469+
/*
470+
|--------------------------------------------------------------------------
471+
| Stripe Payment Configuration
472+
|--------------------------------------------------------------------------
473+
|
474+
| Declare some of the global config values of the Stripe Payments
475+
|
476+
*/
477+
478+
$config['stripe_payment_feature'] = Config::STRIPE_PAYMENT_FEATURE;
479+
$config['stripe_api_key'] = Config::STRIPE_API_KEY;
480+
469481
/* End of file config.php */
470482
/* Location: ./application/config/config.php */

application/controllers/Booking.php

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -201,6 +201,7 @@ public function index()
201201
$provider = $this->providers_model->find($appointment['id_users_provider']);
202202
$customer = $this->customers_model->find($appointment['id_users_customer']);
203203
$customer_token = md5(uniqid(mt_rand(), TRUE));
204+
$is_paid = $appointment['is_paid'];
204205

205206
// Cache the token for 10 minutes.
206207
$this->cache->save('customer-token-' . $customer_token, $customer['id'], 600);
@@ -212,6 +213,7 @@ public function index()
212213
$appointment = NULL;
213214
$provider = NULL;
214215
$customer = NULL;
216+
$is_paid = 0;
215217
}
216218

217219
script_vars([
@@ -270,9 +272,11 @@ public function index()
270272
'grouped_timezones' => $grouped_timezones,
271273
'manage_mode' => $manage_mode,
272274
'customer_token' => $customer_token,
275+
'is_paid' => $is_paid,
273276
'appointment_data' => $appointment,
274277
'provider_data' => $provider,
275278
'customer_data' => $customer,
279+
'company_email' => setting('company_email'),
276280
]);
277281

278282
$this->load->view('pages/booking');

application/controllers/Booking_confirmation.php

Lines changed: 23 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -30,13 +30,13 @@ public function __construct()
3030
$this->load->model('providers_model');
3131
$this->load->model('services_model');
3232
$this->load->model('customers_model');
33-
33+
3434
$this->load->library('google_sync');
3535
}
3636

3737
/**
3838
* Display the appointment registration success page.
39-
*
39+
*
4040
* @throws Exception
4141
*/
4242
public function of()
@@ -54,14 +54,34 @@ public function of()
5454

5555
$appointment = $occurrences[0];
5656

57-
$add_to_google_url = $this->google_sync->get_add_to_google_url($appointment['id']);
57+
$add_to_google_url = $this->google_sync->get_add_to_google_url($appointment['id']);
58+
59+
$service = $this->services_model->find($appointment['id_services']);
60+
61+
$customer = $this->customers_model->find($appointment['id_users_customer']);
62+
63+
$payment_link = null;
64+
if( isset($service['payment_link']) && !empty(trim($service['payment_link']))){
65+
$payment_link_vars = array(
66+
'{$appointment_hash}' => $appointment['hash'],
67+
'{$customer_email}' => $customer['email'],
68+
);
69+
$payment_link_template = $service['payment_link']
70+
. (str_contains($service['payment_link'], '?')
71+
? '' : '?')
72+
. 'client_reference_id={$appointment_hash}&prefilled_email={$customer_email}';
73+
74+
$payment_link = strtr($payment_link_template, $payment_link_vars);
75+
}
5876

5977
html_vars([
6078
'page_title' => lang('success'),
6179
'company_color' => setting('company_color'),
6280
'google_analytics_code' => setting('google_analytics_code'),
6381
'matomo_analytics_url' => setting('matomo_analytics_url'),
6482
'add_to_google_url' => $add_to_google_url,
83+
'is_paid' => $appointment['is_paid'],
84+
'payment_link' => $payment_link,
6585
]);
6686

6787
$this->load->view('pages/booking_confirmation');

application/controllers/Calendar.php

Lines changed: 8 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -134,6 +134,7 @@ public function index(string $appointment_hash = '')
134134
'secretary_providers' => $secretary_providers,
135135
'edit_appointment' => $edit_appointment,
136136
'customers' => $this->customers_model->get(NULL, 50, NULL, 'update_datetime DESC'),
137+
'stripe_payment_feature' => config('stripe_payment_feature'),
137138
]);
138139

139140
html_vars([
@@ -259,6 +260,7 @@ public function save_appointment()
259260
'id_users_provider',
260261
'id_users_customer',
261262
'id_services',
263+
'is_paid',
262264
]);
263265

264266
$appointment['id'] = $this->appointments_model->save($appointment);
@@ -654,9 +656,9 @@ public function get_calendar_appointments()
654656
$end_date = $this->db->escape(date('Y-m-d', strtotime(request('end_date') . ' +1 day')));
655657

656658
$where_clause = $where_id . ' = ' . $record_id . '
657-
AND ((start_datetime > ' . $start_date . ' AND start_datetime < ' . $end_date . ')
658-
or (end_datetime > ' . $start_date . ' AND end_datetime < ' . $end_date . ')
659-
or (start_datetime <= ' . $start_date . ' AND end_datetime >= ' . $end_date . '))
659+
AND ((start_datetime > ' . $start_date . ' AND start_datetime < ' . $end_date . ')
660+
or (end_datetime > ' . $start_date . ' AND end_datetime < ' . $end_date . ')
661+
or (start_datetime <= ' . $start_date . ' AND end_datetime >= ' . $end_date . '))
660662
AND is_unavailability = 0
661663
';
662664

@@ -675,9 +677,9 @@ public function get_calendar_appointments()
675677
if ($filter_type == FILTER_TYPE_PROVIDER)
676678
{
677679
$where_clause = $where_id . ' = ' . $record_id . '
678-
AND ((start_datetime > ' . $start_date . ' AND start_datetime < ' . $end_date . ')
679-
or (end_datetime > ' . $start_date . ' AND end_datetime < ' . $end_date . ')
680-
or (start_datetime <= ' . $start_date . ' AND end_datetime >= ' . $end_date . '))
680+
AND ((start_datetime > ' . $start_date . ' AND start_datetime < ' . $end_date . ')
681+
or (end_datetime > ' . $start_date . ' AND end_datetime < ' . $end_date . ')
682+
or (start_datetime <= ' . $start_date . ' AND end_datetime >= ' . $end_date . '))
681683
AND is_unavailability = 1
682684
';
683685

Lines changed: 245 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,245 @@
1+
<?php defined('BASEPATH') or exit('No direct script access allowed');
2+
3+
/* ----------------------------------------------------------------------------
4+
* Easy!Appointments - Online Appointment Scheduler
5+
*
6+
* @package EasyAppointments
7+
* @author F.González <[email protected]>
8+
* @copyright Copyright (c) Alex Tselegidis
9+
* @license https://opensource.org/licenses/GPL-3.0 - GPLv3
10+
* @link https://easyappointments.org
11+
* @since v1.0.0
12+
* ---------------------------------------------------------------------------- */
13+
14+
/**
15+
* Payment confirmation controller.
16+
*
17+
* Handles the confirmation of a payment.
18+
*
19+
*
20+
* @package Controllers
21+
*/
22+
class Payment extends EA_Controller {
23+
/**
24+
* Booking constructor.
25+
*/
26+
public function __construct()
27+
{
28+
parent::__construct();
29+
30+
$this->load->model('appointments_model');
31+
$this->load->model('providers_model');
32+
$this->load->model('admins_model');
33+
$this->load->model('secretaries_model');
34+
$this->load->model('categories_model');
35+
$this->load->model('services_model');
36+
$this->load->model('customers_model');
37+
$this->load->model('settings_model');
38+
$this->load->model('consents_model');
39+
40+
$this->load->library('timezones');
41+
$this->load->library('synchronization');
42+
$this->load->library('notifications');
43+
$this->load->library('availability');
44+
$this->load->library('webhooks_client');
45+
46+
$this->load->driver('cache', ['adapter' => 'file']);
47+
}
48+
49+
/**
50+
* Render the payment confirmation page.
51+
*/
52+
public function index()
53+
{
54+
if ( ! is_app_installed())
55+
{
56+
redirect('installation');
57+
58+
return;
59+
}
60+
61+
$appointment = html_vars('appointment');
62+
63+
if (empty($appointment)) {
64+
abort(404, "Forbidden");
65+
} else {
66+
$manage_mode = TRUE;
67+
$company_name = setting('company_name');
68+
$company_logo = setting('company_logo');
69+
$company_color = setting('company_color');
70+
$google_analytics_code = setting('google_analytics_code');
71+
$matomo_analytics_url = setting('matomo_analytics_url');
72+
$date_format = setting('date_format');
73+
$time_format = setting('time_format');
74+
75+
$display_first_name = setting('display_first_name');
76+
$require_first_name = setting('require_first_name');
77+
$display_last_name = setting('display_last_name');
78+
$require_last_name = setting('require_last_name');
79+
$display_email = setting('display_email');
80+
$require_email = setting('require_email');
81+
$display_phone_number = setting('display_phone_number');
82+
$require_phone_number = setting('require_phone_number');
83+
$display_address = setting('display_address');
84+
$require_address = setting('require_address');
85+
$display_city = setting('display_city');
86+
$require_city = setting('require_city');
87+
$display_zip_code = setting('display_zip_code');
88+
$require_zip_code = setting('require_zip_code');
89+
$display_notes = setting('display_notes');
90+
$require_notes = setting('require_notes');
91+
$display_cookie_notice = setting('display_cookie_notice');
92+
$cookie_notice_content = setting('cookie_notice_content');
93+
$display_terms_and_conditions = setting('display_terms_and_conditions');
94+
$terms_and_conditions_content = setting('terms_and_conditions_content');
95+
$display_privacy_policy = setting('display_privacy_policy');
96+
$privacy_policy_content = setting('privacy_policy_content');
97+
98+
$theme = request('theme', setting('theme', 'default'));
99+
if (empty($theme) || ! file_exists(__DIR__ . '/../../assets/css/themes/' . $theme . '.min.css'))
100+
{
101+
$theme = 'default';
102+
}
103+
104+
$timezones = $this->timezones->to_array();
105+
$grouped_timezones = $this->timezones->to_grouped_array();
106+
$provider = $this->providers_model->find($appointment['id_users_provider']);
107+
$customer = $this->customers_model->find($appointment['id_users_customer']);
108+
109+
script_vars([
110+
'date_format' => $date_format,
111+
'time_format' => $time_format,
112+
'display_cookie_notice' => $display_cookie_notice,
113+
'display_any_provider' => setting('display_any_provider'),
114+
]);
115+
116+
html_vars([
117+
'theme' => $theme,
118+
'company_name' => $company_name,
119+
'company_logo' => $company_logo,
120+
'company_color' => $company_color === '#ffffff' ? '' : $company_color,
121+
'date_format' => $date_format,
122+
'time_format' => $time_format,
123+
'display_first_name' => $display_first_name,
124+
'display_last_name' => $display_last_name,
125+
'display_email' => $display_email,
126+
'display_phone_number' => $display_phone_number,
127+
'display_address' => $display_address,
128+
'display_city' => $display_city,
129+
'display_zip_code' => $display_zip_code,
130+
'display_notes' => $display_notes,
131+
'google_analytics_code' => $google_analytics_code,
132+
'matomo_analytics_url' => $matomo_analytics_url,
133+
'timezones' => $timezones,
134+
'grouped_timezones' => $grouped_timezones,
135+
'appointment' => $appointment,
136+
'provider' => $provider,
137+
'customer' => $customer,
138+
]);
139+
140+
$this->load->view('pages/payment');
141+
}
142+
}
143+
144+
/**
145+
* Validates Stripe payment and render confirmation screen for the appointment.
146+
*
147+
* This method sets a flag as paid for an appointment and call the "index" callback
148+
* to handle the page rendering.
149+
*
150+
* @param string $checkout_session_id Stripe session id.
151+
*/
152+
public function confirm(string $checkout_session_id)
153+
{
154+
try
155+
{
156+
$stripe_api_key = config('stripe_api_key');
157+
158+
$stripe = new \Stripe\StripeClient($stripe_api_key);
159+
160+
$session = $stripe->checkout->sessions->retrieve($checkout_session_id);
161+
162+
$appointment_hash = $session->client_reference_id;
163+
$payment_intent = $session->payment_intent;
164+
165+
$appointment = $this->set_paid($appointment_hash, $payment_intent);
166+
167+
html_vars(['appointment' => $appointment]);
168+
169+
$this->index();
170+
}
171+
catch (Throwable $e)
172+
{
173+
error_log( $e );
174+
abort(500, 'Internal server error');
175+
}
176+
}
177+
178+
/**
179+
* Sets a paid flag and paid intent for an appointment to track paid bookings.
180+
*/
181+
private function set_paid($appointment_hash, $payment_intent)
182+
{
183+
try
184+
{
185+
$manage_mode = TRUE;
186+
187+
$occurrences = $this->appointments_model->get(['hash' => $appointment_hash]);
188+
189+
if (empty($occurrences))
190+
{
191+
abort(404, 'Not Found');
192+
}
193+
194+
$appointment = $occurrences[0];
195+
196+
$provider = $this->providers_model->find($appointment['id_users_provider']);
197+
198+
$customer = $this->customers_model->find($appointment['id_users_customer']);
199+
200+
$service = $this->services_model->find($appointment['id_services']);
201+
202+
203+
$appointment['is_paid'] = 1;
204+
$appointment['payment_intent'] = $payment_intent;
205+
$this->appointments_model->only($appointment, [
206+
'id',
207+
'start_datetime',
208+
'end_datetime',
209+
'location',
210+
'notes',
211+
'color',
212+
'is_unavailability',
213+
'id_users_provider',
214+
'id_users_customer',
215+
'id_services',
216+
'is_paid',
217+
'payment_intent',
218+
]);
219+
$appointment_id = $this->appointments_model->save($appointment);
220+
$appointment = $this->appointments_model->find($appointment_id);
221+
222+
$settings = [
223+
'company_name' => setting('company_name'),
224+
'company_link' => setting('company_link'),
225+
'company_email' => setting('company_email'),
226+
'date_format' => setting('date_format'),
227+
'time_format' => setting('time_format')
228+
];
229+
230+
$this->synchronization->sync_appointment_saved($appointment, $service, $provider, $customer, $settings, $manage_mode);
231+
232+
$this->notifications->notify_appointment_saved($appointment, $service, $provider, $customer, $settings, $manage_mode);
233+
234+
$this->webhooks_client->trigger(WEBHOOK_APPOINTMENT_SAVE, $appointment);
235+
236+
return $appointment;
237+
}
238+
catch (Throwable $e)
239+
{
240+
error_log( $e );
241+
abort(500, 'Internal server error');
242+
}
243+
}
244+
245+
}

0 commit comments

Comments
 (0)