Skip to content

Commit 24ee1d3

Browse files
author
Fernando González
committed
Adding Stripe link payments
1 parent 0b5eacb commit 24ee1d3

File tree

62 files changed

+1164
-13
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

+1164
-13
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
@@ -457,5 +457,17 @@
457457
*/
458458
$config['rate_limiting'] = true;
459459

460+
/*
461+
|--------------------------------------------------------------------------
462+
| Stripe Payment Configuration
463+
|--------------------------------------------------------------------------
464+
|
465+
| Declare some of the global config values of the Stripe Payments
466+
|
467+
*/
468+
469+
$config['stripe_payment_feature'] = Config::STRIPE_PAYMENT_FEATURE;
470+
$config['stripe_api_key'] = Config::STRIPE_API_KEY;
471+
460472
/* End of file config.php */
461473
/* Location: ./application/config/config.php */

application/controllers/Booking.php

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -209,6 +209,7 @@ public function index(): void
209209
$provider = $this->providers_model->find($appointment['id_users_provider']);
210210
$customer = $this->customers_model->find($appointment['id_users_customer']);
211211
$customer_token = md5(uniqid(mt_rand(), true));
212+
$is_paid = $appointment['is_paid'];
212213

213214
// Cache the token for 10 minutes.
214215
$this->cache->save('customer-token-' . $customer_token, $customer['id'], 600);
@@ -218,6 +219,7 @@ public function index(): void
218219
$appointment = null;
219220
$provider = null;
220221
$customer = null;
222+
$is_paid = 0;
221223
}
222224

223225
script_vars([
@@ -276,9 +278,11 @@ public function index(): void
276278
'grouped_timezones' => $grouped_timezones,
277279
'manage_mode' => $manage_mode,
278280
'customer_token' => $customer_token,
281+
'is_paid' => $is_paid,
279282
'appointment_data' => $appointment,
280283
'provider_data' => $provider,
281284
'customer_data' => $customer,
285+
'company_email' => setting('company_email'),
282286
]);
283287

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

application/controllers/Booking_confirmation.php

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,12 +56,32 @@ public function of(): void
5656

5757
$add_to_google_url = $this->google_sync->get_add_to_google_url($appointment['id']);
5858

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+
}
76+
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
@@ -145,6 +145,7 @@ public function index(string $appointment_hash = ''): void
145145
'secretary_providers' => $secretary_providers,
146146
'edit_appointment' => $edit_appointment,
147147
'customers' => $this->customers_model->get(null, 50, null, 'update_datetime DESC'),
148+
'stripe_payment_feature' => config('stripe_payment_feature'),
148149
]);
149150

150151
html_vars([
@@ -255,6 +256,7 @@ public function save_appointment(): void
255256
'id_users_provider',
256257
'id_users_customer',
257258
'id_services',
259+
'is_paid',
258260
]);
259261

260262
$appointment['id'] = $this->appointments_model->save($appointment);
@@ -654,17 +656,17 @@ public function get_calendar_appointments(): void
654656
$start_date .
655657
' AND start_datetime < ' .
656658
$end_date .
657-
')
659+
')
658660
or (end_datetime > ' .
659661
$start_date .
660662
' AND end_datetime < ' .
661663
$end_date .
662-
')
664+
')
663665
or (start_datetime <= ' .
664666
$start_date .
665667
' AND end_datetime >= ' .
666668
$end_date .
667-
'))
669+
'))
668670
AND is_unavailability = 0
669671
';
670672

@@ -691,17 +693,17 @@ public function get_calendar_appointments(): void
691693
$start_date .
692694
' AND start_datetime < ' .
693695
$end_date .
694-
')
696+
')
695697
or (end_datetime > ' .
696698
$start_date .
697699
' AND end_datetime < ' .
698700
$end_date .
699-
')
701+
')
700702
or (start_datetime <= ' .
701703
$start_date .
702704
' AND end_datetime >= ' .
703705
$end_date .
704-
'))
706+
'))
705707
AND is_unavailability = 1
706708
';
707709

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 <fernando@tuta.io>
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)