Various changes and fixes (#7424)
[openemr.git] / interface / patient_file / front_payment_terminal.php
blob98120d6859b2be9691ed6c625fd0346a9a0c31df
1 <?php
2 require_once(__DIR__ . "/../globals.php");
4 use OpenEMR\Core\Header;
6 $total = $_GET['total'] ?? null;
7 ?>
9 <!DOCTYPE html>
10 <html lang="en">
11 <head>
12 <meta charset="utf-8" />
13 <title><?php echo xlt("POS Payments") ?></title>
14 <meta name="description" content="In-person payment on Stripe" />
15 <meta name="viewport" content="width=device-width, initial-scale=1" />
16 <?php Header::setupHeader(['opener']); ?>
17 <script src="https://js.stripe.com/terminal/v1/"></script>
18 <script>
19 let amount = <?php echo js_escape(str_replace('.', '', $total)); ?>;
20 // run anonymous function to get invoice data for metadata.
21 const encDates = (() => {
22 let i = 0, c;
23 let invDates = '';
24 opener.$('#table_display tbody tr').each(function () {
25 if (this.className == 'table-active') {
26 return false;
28 if(i > 4) {
29 return false; // breaks on max 5 encounters
31 invDates += 'item' + ++i + ': ';
32 c = 0;
33 $(this).find('td').each(function() {
34 if (++c < 3) {
35 invDates += this.innerText + ' ';
37 });
38 });
39 return invDates;
40 })();
42 const terminal = StripeTerminal.create({
43 onFetchConnectionToken: fetchConnectionToken,
44 onUnexpectedReaderDisconnect: unexpectedDisconnect,
45 });
47 function unexpectedDisconnect() {
48 alert("Disconnected from reader");
49 console.log("Disconnected from reader");
52 function fetchConnectionToken() {
53 // The SDK manages the ConnectionToken's lifecycle.
54 return fetch('./front_payment_cc.php?mode=terminal_token', {method: "POST"}).then(function (response) {
55 return response.json();
56 }).then(function (data) {
57 return data.secret;
58 });
61 let discoveredReaders;
62 let customerId;
63 // Handler for a "Discover readers" button
64 function discoverReaderHandler() {
65 const config = {simulated: false};
66 terminal.discoverReaders(config).then(function (discoverResult) {
67 if (discoverResult.error) {
68 alert(discoverResult.error.message);
69 console.log('Failed to discover: ', discoverResult.error);
70 } else if (discoverResult.discoveredReaders.length === 0) {
71 alert(xl('No available readers.'));
72 } else {
73 discoveredReaders = discoverResult.discoveredReaders;
74 log('Terminal Discover Reader', discoveredReaders);
75 connectReaderHandler(discoveredReaders);
77 });
80 // Handler for a "Connect Reader" button
81 function connectReaderHandler(discoveredReaders) {
82 // Just select the first reader here.
83 if (!discoveredReaders) {
84 alert(xl("Error No selected Readers"));
85 return false;
87 const selectedReader = discoveredReaders[0];
88 terminal.connectReader(selectedReader).then(function (connectResult) {
89 if (connectResult.error) {
90 alert(connectResult.error.message);
91 console.log('Failed to connect: ', connectResult.error);
92 } else {
93 document.getElementById("collect-button").classList.remove("d-none");
94 console.log('Connected to reader: ', connectResult.reader.label);
95 log('Connect to Reader', connectResult)
97 });
100 function fetchPaymentIntentClientSecret(amount) {
101 const bodyContent = JSON.stringify({
102 amount: amount,
103 encs: encDates
105 return fetch('./front_payment_cc.php?mode=terminal_create', {
106 method: "POST",
107 headers: {
108 'Content-Type': 'application/json'
110 body: bodyContent
111 }).then(function (response) {
112 return response.json();
113 }).then(function (data) {
114 return data.client_secret;
118 let paymentIntentId;
119 let isChargePending = false;
121 function collectPayment(amount) {
122 try {
123 fetchPaymentIntentClientSecret(amount).then(function (client_secret) {
124 terminal.collectPaymentMethod(client_secret).then(function (result) {
125 if (result.error) {
126 alert(result.error.message);
127 } else {
128 log('Collect Payment Method', result.paymentIntent);
129 terminal.processPayment(result.paymentIntent).then(function (result) {
130 if (result.error) {
131 console.log(result.error);
132 alert(result.error.message);
133 } else if (result.paymentIntent) {
134 paymentIntentId = result.paymentIntent.id;
135 log('Process Payment', result.paymentIntent);
136 isChargePending = true;
137 document.getElementById("collect-button").classList.add("d-none");
138 document.getElementById("capture-button").classList.remove("d-none");
139 document.getElementById("refund-button").classList.remove("d-none");
145 } catch (e) {
146 alert(e.message);
150 function capture(paymentIntentId) {
151 return fetch('./front_payment_cc.php?mode=terminal_capture', {
152 method: "POST",
153 headers: {
154 'Content-Type': 'application/json'
156 body: JSON.stringify({"id": paymentIntentId})
157 }).then(function (response) {
158 return response.json();
159 }).then(function (data) {
160 if (data.error) {
161 log('Capture Payment Error', data.error);
162 console.log(data.error);
163 alert(data.error);
164 return false;
166 opener.document.getElementById("check_number").value = data.id;
167 opener.$("[name='form_save']").click();
168 dlgclose();
172 function cancel(paymentIntentId) {
173 return fetch('./front_payment_cc.php?mode=cancel_intent', {
174 method: "POST",
175 headers: {
176 'Content-Type': 'application/json'
178 body: JSON.stringify({"id": paymentIntentId})
179 }).then(function (response) {
180 return response.json();
181 }).then(function (data) {
182 if (data.error) {
183 log('Cancel Payment Error', data.error);
184 console.log(data.error);
185 alert(data.error);
186 return false;
188 isChargePending = false;
189 document.getElementById("refund-button").classList.add("d-none");
190 document.getElementById("collect-button").classList.remove("d-none");
191 document.getElementById("capture-button").classList.add("d-none");
192 log('Cancel Payment', data.status);
196 $(function () {
197 const collectButton = document.getElementById('collect-button');
198 collectButton.addEventListener('click', async (event) => {
199 collectPayment(amount);
202 const captureButton = document.getElementById('capture-button');
203 captureButton.addEventListener('click', async (event) => {
204 capture(paymentIntentId);
207 const cancelIntentButton = document.getElementById('refund-button');
208 cancelIntentButton.addEventListener('click', async (event) => {
209 cancel(paymentIntentId);
212 const cancelButton = parent.document.getElementById('closeBtn');
213 cancelButton.addEventListener('click', async (event) => {
214 if (isChargePending) {
215 if (confirm(xl("There is a charge transaction that has not been captured." + "\n" + xl("Are you sure?")))) {
216 dlgclose();
218 return false;
220 dlgclose();
222 // get/init reader
223 discoverReaderHandler();
226 function log(method, message) {
227 let logs = document.getElementById("logs");
228 let title = document.createElement("div");
229 let log = document.createElement("div");
230 let lineCol = document.createElement("div");
231 let logCol = document.createElement("div");
232 title.classList.add('row');
233 title.classList.add('log-title');
234 title.textContent = method;
235 log.classList.add('row');
236 log.classList.add('log');
237 let hr = document.createElement("hr");
238 let pre = document.createElement("pre");
239 let code = document.createElement("code");
240 code.textContent = formatJson(JSON.stringify(message, undefined, 2));
241 pre.append(code);
242 log.append(pre);
243 logs.prepend(hr);
244 logs.prepend(log);
245 logs.prepend(title);
248 function stringLengthOfInt(number) {
249 return number.toString().length;
252 function padSpaces(lineNumber, fixedWidth) {
253 // Always indent by 2 and then maybe more, based on the width of the line
254 // number.
255 return " ".repeat(2 + fixedWidth - stringLengthOfInt(lineNumber));
258 function formatJson(message) {
259 let lines = message.split('\n');
260 let json = "";
261 let lineNumberFixedWidth = stringLengthOfInt(lines.length);
262 for (let i = 1; i <= lines.length; i += 1) {
263 line = i + padSpaces(i, lineNumberFixedWidth) + lines[i - 1];
264 json = json + line + '\n';
266 return json
268 </script>
269 </head>
270 <body>
271 <div class="container-fluid ">
272 <div class="row">
273 <div class="col-sm-6 offset-sm-3">
274 <h4><span class="m-1"><?php echo xlt("Paying Amount") ?></span><i>$</i><span><?php echo text($total); ?></span></h4>
275 </div>
276 </div>
277 <div class="row m-1">
278 <button id="collect-button" class="btn btn-primary btn-transmit m-1 d-none"><?php echo xlt("Collect Payment")?></button>
279 <button id="capture-button" class="btn btn-primary btn-transmit m-1 d-none"><?php echo xlt("Post Payment")?></button>
280 <button id="refund-button" class="btn btn-primary btn-transmit m-1 d-none"><?php echo xlt("Cancel Payment")?></button>
281 </div>
282 <hr />
283 <div class="row ml-2"><h5><?php echo xlt("Transaction Progress") ?></h5></div>
284 <div class="col-sm-12 p-2 bg-secondary" id="logs"><i class="fa fa-spinner fa-spin fa-2x"></i></div>
286 </div>
287 </body>
288 </html>