dolibarr 21.0.0-alpha
ipn.php
1<?php
2/* Copyright (C) 2018-2020 Thibault FOUCART <support@ptibogxiv.net>
3 * Copyright (C) 2018-2024 Frédéric France <frederic.france@free.fr>
4 * Copyright (C) 2023 Laurent Destailleur <eldy@users.sourceforge.net>
5 * Copyright (C) 2024 MDW <mdeweerd@users.noreply.github.com>
6 *
7 * This program is free software; you can redistribute it and/or modify
8 * it under the terms of the GNU General Public License as published by
9 * the Free Software Foundation; either version 3 of the License, or
10 * (at your option) any later version.
11 *
12 * This program is distributed in the hope that it will be useful,
13 * but WITHOUT ANY WARRANTY; without even the implied warranty of
14 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
15 * GNU General Public License for more details.
16 *
17 * You should have received a copy of the GNU General Public License
18 * along with this program. If not, see <https://www.gnu.org/licenses/>.
19 */
20
21if (!defined('NOLOGIN')) {
22 define("NOLOGIN", 1); // This means this output page does not require to be logged.
23}
24if (!defined('NOCSRFCHECK')) {
25 define("NOCSRFCHECK", 1); // We accept to go on this page from external web site.
26}
27if (!defined('NOIPCHECK')) {
28 define('NOIPCHECK', '1'); // Do not check IP defined into conf $dolibarr_main_restrict_ip
29}
30if (!defined('NOBROWSERNOTIF')) {
31 define('NOBROWSERNOTIF', '1');
32}
33
34// Because 2 entities can have the same ref.
35$entity = (!empty($_GET['entity']) ? (int) $_GET['entity'] : (!empty($_POST['entity']) ? (int) $_POST['entity'] : 1));
36if (is_numeric($entity)) {
37 define("DOLENTITY", $entity);
38}
39
40// So log file will have a suffix
41if (!defined('USESUFFIXINLOG')) {
42 define('USESUFFIXINLOG', '_stripeipn');
43}
44
45// Load Dolibarr environment
46require '../../main.inc.php';
47require_once DOL_DOCUMENT_ROOT.'/core/lib/admin.lib.php';
48require_once DOL_DOCUMENT_ROOT.'/user/class/user.class.php';
49require_once DOL_DOCUMENT_ROOT.'/core/class/ccountry.class.php';
50require_once DOL_DOCUMENT_ROOT.'/commande/class/commande.class.php';
51require_once DOL_DOCUMENT_ROOT.'/compta/paiement/class/paiement.class.php';
52require_once DOL_DOCUMENT_ROOT.'/compta/facture/class/facture.class.php';
53require_once DOL_DOCUMENT_ROOT.'/compta/bank/class/account.class.php';
54require_once DOL_DOCUMENT_ROOT.'/compta/prelevement/class/bonprelevement.class.php';
55require_once DOL_DOCUMENT_ROOT.'/societe/class/societe.class.php';
56require_once DOL_DOCUMENT_ROOT.'/core/class/CMailFile.class.php';
57require_once DOL_DOCUMENT_ROOT.'/includes/stripe/stripe-php/init.php';
58require_once DOL_DOCUMENT_ROOT.'/stripe/class/stripe.class.php';
59
60
61// You can find your endpoint's secret in your webhook settings
62if (GETPOSTISSET('connect')) {
63 if (GETPOSTISSET('test')) {
64 $endpoint_secret = getDolGlobalString('STRIPE_TEST_WEBHOOK_CONNECT_KEY');
65 $service = 'StripeTest';
66 $servicestatus = 0;
67 } else {
68 $endpoint_secret = getDolGlobalString('STRIPE_LIVE_WEBHOOK_CONNECT_KEY');
69 $service = 'StripeLive';
70 $servicestatus = 1;
71 }
72} else {
73 if (GETPOSTISSET('test')) {
74 $endpoint_secret = getDolGlobalString('STRIPE_TEST_WEBHOOK_KEY');
75 $service = 'StripeTest';
76 $servicestatus = 0;
77 } else {
78 $endpoint_secret = getDolGlobalString('STRIPE_LIVE_WEBHOOK_KEY');
79 $service = 'StripeLive';
80 $servicestatus = 1;
81 }
82}
83
84if (!isModEnabled('stripe')) {
85 httponly_accessforbidden('Module Stripe not enabled');
86}
87
88if (empty($endpoint_secret)) {
89 httponly_accessforbidden('Error: Setup of module Stripe not complete for mode '.dol_escape_htmltag($service).'. The WEBHOOK_KEY is not defined.', 400, 1);
90}
91
92if (getDolGlobalString('STRIPE_USER_ACCOUNT_FOR_ACTIONS')) {
93 // We set the user to use for all ipn actions in Dolibarr
94 $user = new User($db);
95 $user->fetch(getDolGlobalString('STRIPE_USER_ACCOUNT_FOR_ACTIONS'));
96 $user->loadRights();
97} else {
98 httponly_accessforbidden('Error: Setup of module Stripe not complete for mode '.dol_escape_htmltag($service).'. The STRIPE_USER_ACCOUNT_FOR_ACTIONS is not defined.', 400, 1);
99}
100
101$now = dol_now();
102
103// Security
104// The test on security key is done later into constructEvent() method.
105
106
107/*
108 * Actions
109 */
110
111$payload = @file_get_contents("php://input");
112$sig_header = empty($_SERVER["HTTP_STRIPE_SIGNATURE"]) ? '' : $_SERVER["HTTP_STRIPE_SIGNATURE"];
113$event = null;
114
115if (getDolGlobalString('STRIPE_DEBUG')) {
116 $fh = fopen(DOL_DATA_ROOT.'/dolibarr_stripeipn_payload.log', 'w+');
117 if ($fh) {
118 fwrite($fh, dol_print_date(dol_now('gmt'), 'standard').' IPN Called. service='.$service.' HTTP_STRIPE_SIGNATURE='.$sig_header."\n");
119 fwrite($fh, $payload);
120 fclose($fh);
121 dolChmod(DOL_DATA_ROOT.'/dolibarr_stripeipn_payload.log');
122 }
123}
124
125$error = 0;
126
127try {
128 $event = \Stripe\Webhook::constructEvent($payload, $sig_header, $endpoint_secret);
129} catch (UnexpectedValueException $e) {
130 // Invalid payload
131 httponly_accessforbidden('Invalid payload', 400);
132} catch (\Stripe\Exception\SignatureVerificationException $e) {
133 httponly_accessforbidden('Invalid signature. May be a hook for an event created by another Stripe env ? Check setup of your keys whsec_...', 400);
134} catch (Exception $e) {
135 httponly_accessforbidden('Error '.$e->getMessage(), 400);
136}
137
138// Do something with $event
139
140$langs->load("main");
141
142
143if (isModEnabled('multicompany') && !empty($conf->stripeconnect->enabled) && is_object($mc)) {
144 $sql = "SELECT entity";
145 $sql .= " FROM ".MAIN_DB_PREFIX."oauth_token";
146 $sql .= " WHERE service = '".$db->escape($service)."' and tokenstring LIKE '%".$db->escape($db->escapeforlike($event->account))."%'";
147
148 dol_syslog(get_class($db)."::fetch", LOG_DEBUG);
149 $result = $db->query($sql);
150 if ($result) {
151 if ($db->num_rows($result)) {
152 $obj = $db->fetch_object($result);
153 $key = $obj->entity;
154 } else {
155 $key = 1;
156 }
157 } else {
158 $key = 1;
159 }
160 $ret = $mc->switchEntity($key);
161}
162
163// list of action
164$stripe = new Stripe($db);
165
166// Subject
167$societeName = getDolGlobalString('MAIN_INFO_SOCIETE_NOM');
168if (getDolGlobalString('MAIN_APPLICATION_TITLE')) {
169 $societeName = getDolGlobalString('MAIN_APPLICATION_TITLE');
170}
171
173
174dol_syslog("***** Stripe IPN was called with event->type=".$event->type." service=".$service);
175
176
177if ($event->type == 'payout.created') {
178 // When a payout is create by Stripe to transfer money to your account
179 $error = 0;
180
181 $result = dolibarr_set_const($db, $service."_NEXTPAYOUT", date('Y-m-d H:i:s', $event->data->object->arrival_date), 'chaine', 0, '', $conf->entity);
182
183 if ($result > 0) {
184 $subject = $societeName.' - [NOTIFICATION] Stripe payout scheduled';
185 if (!empty($user->email)) {
186 $sendto = dolGetFirstLastname($user->firstname, $user->lastname)." <".$user->email.">";
187 } else {
188 $sendto = getDolGlobalString('MAIN_INFO_SOCIETE_MAIL') . '" <' . getDolGlobalString('MAIN_INFO_SOCIETE_MAIL').'>';
189 }
190 $replyto = $sendto;
191 $sendtocc = '';
192 if (getDolGlobalString('ONLINE_PAYMENT_SENDEMAIL')) {
193 $sendtocc = getDolGlobalString('ONLINE_PAYMENT_SENDEMAIL') . '" <' . getDolGlobalString('ONLINE_PAYMENT_SENDEMAIL').'>';
194 }
195
196 $message = "A bank transfer of ".price2num($event->data->object->amount / 100)." ".$event->data->object->currency." should arrive in your account the ".dol_print_date($event->data->object->arrival_date, 'dayhour');
197
198 $mailfile = new CMailFile(
199 $subject,
200 $sendto,
201 $replyto,
202 $message,
203 array(),
204 array(),
205 array(),
206 $sendtocc,
207 '',
208 0,
209 -1
210 );
211
212 $ret = $mailfile->sendfile();
213
214 return 1;
215 } else {
216 $error++;
217 http_response_code(500);
218 return -1;
219 }
220} elseif ($event->type == 'payout.paid') {
221 // When a payout to transfer money to your account is completely done
222 $error = 0;
223 $result = dolibarr_set_const($db, $service."_NEXTPAYOUT", 0, 'chaine', 0, '', $conf->entity);
224 if ($result) {
225 $langs->load("errors");
226
227 $dateo = dol_now();
228 $label = $event->data->object->description;
229 $amount = $event->data->object->amount / 100;
230 $amount_to = $event->data->object->amount / 100;
231 require_once DOL_DOCUMENT_ROOT.'/compta/bank/class/account.class.php';
232
233 $accountfrom = new Account($db);
234 $accountfrom->fetch(getDolGlobalInt('STRIPE_BANK_ACCOUNT_FOR_PAYMENTS'));
235
236 $accountto = new Account($db);
237 $accountto->fetch(getDolGlobalInt('STRIPE_BANK_ACCOUNT_FOR_BANKTRANSFERS'));
238
239 if (($accountto->id != $accountfrom->id) && empty($error)) {
240 $bank_line_id_from = 0;
241 $bank_line_id_to = 0;
242 $result = 0;
243
244 // By default, electronic transfer from bank to bank
245 $typefrom = 'PRE';
246 $typeto = 'VIR';
247
248 if (!$error) {
249 $bank_line_id_from = $accountfrom->addline($dateo, $typefrom, $label, -1 * (float) price2num($amount), '', '', $user);
250 }
251 if (!($bank_line_id_from > 0)) {
252 $error++;
253 }
254 if (!$error) {
255 $bank_line_id_to = $accountto->addline($dateo, $typeto, $label, price2num($amount), '', '', $user);
256 }
257 if (!($bank_line_id_to > 0)) {
258 $error++;
259 }
260
261 if (!$error) {
262 $result = $accountfrom->add_url_line($bank_line_id_from, $bank_line_id_to, DOL_URL_ROOT.'/compta/bank/line.php?rowid=', '(banktransfert)', 'banktransfert');
263 }
264 if (!($result > 0)) {
265 $error++;
266 }
267 if (!$error) {
268 $result = $accountto->add_url_line($bank_line_id_to, $bank_line_id_from, DOL_URL_ROOT.'/compta/bank/line.php?rowid=', '(banktransfert)', 'banktransfert');
269 }
270 if (!($result > 0)) {
271 $error++;
272 }
273 }
274
275 $subject = $societeName.' - [NOTIFICATION] Stripe payout done';
276 if (!empty($user->email)) {
277 $sendto = dolGetFirstLastname($user->firstname, $user->lastname)." <".$user->email.">";
278 } else {
279 $sendto = getDolGlobalString('MAIN_INFO_SOCIETE_MAIL') . '" <' . getDolGlobalString('MAIN_INFO_SOCIETE_MAIL').'>';
280 }
281 $replyto = $sendto;
282 $sendtocc = '';
283 if (getDolGlobalString('ONLINE_PAYMENT_SENDEMAIL')) {
284 $sendtocc = getDolGlobalString('ONLINE_PAYMENT_SENDEMAIL') . '" <' . getDolGlobalString('ONLINE_PAYMENT_SENDEMAIL').'>';
285 }
286
287 $message = "A bank transfer of ".price2num($event->data->object->amount / 100)." ".$event->data->object->currency." has been done to your account the ".dol_print_date($event->data->object->arrival_date, 'dayhour');
288
289 $mailfile = new CMailFile(
290 $subject,
291 $sendto,
292 $replyto,
293 $message,
294 array(),
295 array(),
296 array(),
297 $sendtocc,
298 '',
299 0,
300 -1
301 );
302
303 $ret = $mailfile->sendfile();
304
305 return 1;
306 } else {
307 $error++;
308 http_response_code(500);
309 return -1;
310 }
311} elseif ($event->type == 'customer.source.created') {
312 //TODO: save customer's source
313} elseif ($event->type == 'customer.source.updated') {
314 //TODO: update customer's source
315} elseif ($event->type == 'customer.source.delete') {
316 //TODO: delete customer's source
317} elseif ($event->type == 'customer.deleted') {
318 // When a customer account is delete on Stripe side
319 $db->begin();
320 $sql = "DELETE FROM ".MAIN_DB_PREFIX."societe_account WHERE key_account = '".$db->escape($event->data->object->id)."' and site='stripe'";
321 $db->query($sql);
322 $db->commit();
323} elseif ($event->type == 'payment_intent.succeeded') {
324 // Called when making payment with PaymentIntent method ($conf->global->STRIPE_USE_NEW_CHECKOUT is on).
325
326 //dol_syslog("object = ".var_export($event->data, true));
327 include_once DOL_DOCUMENT_ROOT . '/compta/paiement/class/paiement.class.php';
328 global $stripearrayofkeysbyenv;
329 $error = 0;
330 $object = $event->data->object;
331 $TRANSACTIONID = $object->id; // Example pi_123456789...
332 $ipaddress = $object->metadata->ipaddress;
333 $now = dol_now();
334 $currencyCodeType = strtoupper($object->currency);
335 $paymentmethodstripeid = $object->payment_method;
336 $customer_id = $object->customer;
337 $invoice_id = "";
338 $paymentTypeCode = ""; // payment type according to Stripe
339 $paymentTypeCodeInDolibarr = ""; // payment type according to Dolibarr
340 $payment_amount = 0;
341 $payment_amountInDolibarr = 0;
342
343 dol_syslog("Try to find a payment in database for the payment_intent id = ".$TRANSACTIONID);
344
345 $sql = "SELECT pi.rowid, pi.fk_facture, pi.fk_prelevement_bons, pi.amount, pi.type, pi.traite";
346 $sql .= " FROM ".MAIN_DB_PREFIX."prelevement_demande as pi";
347 $sql .= " WHERE pi.ext_payment_id = '".$db->escape($TRANSACTIONID)."'";
348 $sql .= " AND pi.ext_payment_site = '".$db->escape($service)."'";
349
350 $result = $db->query($sql);
351 if ($result) {
352 $obj = $db->fetch_object($result);
353 if ($obj) {
354 if ($obj->type == 'ban') {
355 if ($obj->traite == 1) {
356 // This is a direct-debit with an order (llx_bon_prelevement) ALREADY generated, so
357 // it means we received here the confirmation that payment request is finished.
358 $pdid = $obj->rowid;
359 $invoice_id = $obj->fk_facture;
360 $directdebitorcreditransfer_id = $obj->fk_prelevement_bons;
361 $payment_amountInDolibarr = $obj->amount;
362 $paymentTypeCodeInDolibarr = $obj->type;
363
364 dol_syslog("Found a request in database to pay with direct debit generated (pdid = ".$pdid." directdebitorcreditransfer_id=".$directdebitorcreditransfer_id.")");
365 } else {
366 dol_syslog("Found a request in database not yet generated (pdid = ".$pdid." directdebitorcreditransfer_id=".$directdebitorcreditransfer_id."). Was the order deleted after being sent ?", LOG_WARNING);
367 }
368 }
369 if ($obj->type == 'card' || empty($obj->type)) {
370 if ($obj->traite == 0) {
371 // This is a card payment not already flagged as sent to Stripe.
372 $pdid = $obj->rowid;
373 $invoice_id = $obj->fk_facture;
374 $payment_amountInDolibarr = $obj->amount;
375 $paymentTypeCodeInDolibarr = empty($obj->type) ? 'card' : $obj->type;
376
377 dol_syslog("Found a request in database to pay with card (pdid = ".$pdid."). We should fix status traite to 1");
378 } else {
379 dol_syslog("Found a request in database to pay with card (pdid = ".$pdid.") already set to traite=1. Nothing to fix.");
380 }
381 }
382 } else {
383 dol_syslog("Payment intent ".$TRANSACTIONID." not found into database, so ignored.");
384 http_response_code(200);
385 print "Payment intent ".$TRANSACTIONID." not found into database, so ignored.";
386 return 1;
387 }
388 } else {
389 http_response_code(500);
390 print $db->lasterror();
391 return -1;
392 }
393
394 if ($paymentTypeCodeInDolibarr) {
395 // Here, we need to do something. A $invoice_id has been found.
396
397 $stripeacc = $stripearrayofkeysbyenv[$servicestatus]['secret_key'];
398
399 dol_syslog("Get the Stripe payment object for the payment method id = ".json_encode($paymentmethodstripeid));
400
401 $s = new \Stripe\StripeClient($stripeacc);
402
403 $paymentmethodstripe = $s->paymentMethods->retrieve($paymentmethodstripeid);
404 $paymentTypeCode = $paymentmethodstripe->type;
405 if ($paymentTypeCode == "ban" || $paymentTypeCode == "sepa_debit") {
406 $paymentTypeCode = "PRE";
407 } elseif ($paymentTypeCode == "card") {
408 $paymentTypeCode = "CB";
409 }
410
411 $payment_amount = $payment_amountInDolibarr;
412 // TODO Check payment_amount in Stripe (received) is same than the one in Dolibarr
413
414 $postactionmessages = array();
415
416 if ($paymentTypeCode == "CB" && ($paymentTypeCodeInDolibarr == 'card' || empty($paymentTypeCodeInDolibarr))) {
417 // Case payment type in Stripe and into prelevement_demande are both CARD.
418 // For this case, payment should already have been recorded so we just update flag of payment request if not yet 1
419
420 // TODO Set traite to 1
421 dol_syslog("TODO update flag traite to 1");
422 } elseif ($paymentTypeCode == "PRE" && $paymentTypeCodeInDolibarr == 'ban') {
423 // Case payment type in Stripe and into prelevement_demande are both BAN.
424 // For this case, payment on invoice (not yet recorded) must be done and direct debit order must be closed.
425
426 $paiement = new Paiement($db);
427 $paiement->datepaye = $now;
428 $paiement->date = $now;
429 if ($currencyCodeType == $conf->currency) {
430 $paiement->amounts = [$invoice_id => $payment_amount]; // Array with all payments dispatching with invoice id
431 } else {
432 $paiement->multicurrency_amounts = [$invoice_id => $payment_amount]; // Array with all payments dispatching
433
434 $postactionmessages[] = 'Payment was done in a currency ('.$currencyCodeType.') other than the expected currency of company ('.$conf->currency.')';
435 $ispostactionok = -1;
436 // Not yet supported, so error
437 $error++;
438 }
439
440 // Get ID of payment PRE
441 $paiement->paiementcode = $paymentTypeCode;
442 $sql = "SELECT id FROM ".MAIN_DB_PREFIX."c_paiement";
443 $sql .= " WHERE code = '".$db->escape($paymentTypeCode)."'";
444 $sql .= " AND entity IN (".getEntity('c_paiement').")";
445 $resql = $db->query($sql);
446 if ($resql) {
447 $obj = $db->fetch_object($resql);
448 $paiement->paiementid = $obj->id;
449 } else {
450 $error++;
451 }
452
453 $paiement->num_payment = '';
454 $paiement->note_public = '';
455 $paiement->note_private = 'StripeSepa payment received by IPN webhook - ' . dol_print_date($now, 'standard') . ' (TZ server) using servicestatus=' . $servicestatus . ($ipaddress ? ' from ip ' . $ipaddress : '') . ' - Transaction ID = ' . $TRANSACTIONID;
456 $paiement->ext_payment_id = $TRANSACTIONID.':'.$customer_id.'@'.$stripearrayofkeysbyenv[$servicestatus]['publishable_key']; // May be we should store py_... instead of pi_... but we started with pi_... so we continue.
457 $paiement->ext_payment_site = $service;
458
459 $ispaymentdone = 0;
460 $sql = "SELECT p.rowid FROM ".MAIN_DB_PREFIX."paiement as p";
461 $sql .= " WHERE p.ext_payment_id = '".$db->escape($paiement->ext_payment_id)."'";
462 $sql .= " AND p.ext_payment_site = '".$db->escape($paiement->ext_payment_site)."'";
463 $result = $db->query($sql);
464 if ($result) {
465 if ($db->num_rows($result)) {
466 $ispaymentdone = 1;
467 dol_syslog('* Payment for ext_payment_id '.$paiement->ext_payment_id.' already done. We do not recreate the payment');
468 }
469 }
470
471 $db->begin();
472
473 if (!$error && !$ispaymentdone) {
474 dol_syslog('* Record payment type PRE for invoice id ' . $invoice_id . '. It includes closing of invoice and regenerating document.');
475
476 // This include closing invoices to 'paid' (and trigger including unsuspending) and regenerating document
477 $paiement_id = $paiement->create($user, 1);
478 if ($paiement_id < 0) {
479 $postactionmessages[] = $paiement->error . ($paiement->error ? ' ' : '') . implode("<br>\n", $paiement->errors);
480 $ispostactionok = -1;
481 $error++;
482
483 dol_syslog("Failed to create the payment for invoice id " . $invoice_id);
484 } else {
485 $postactionmessages[] = 'Payment created';
486
487 dol_syslog("The payment has been created for invoice id " . $invoice_id);
488 }
489 }
490
491 if (!$error && isModEnabled('bank')) {
492 // Search again the payment to see if it is already linked to a bank payment record (We should always find the payment that was created before).
493 $ispaymentdone = 0;
494 $sql = "SELECT p.rowid, p.fk_bank FROM ".MAIN_DB_PREFIX."paiement as p";
495 $sql .= " WHERE p.ext_payment_id = '".$db->escape($paiement->ext_payment_id)."'";
496 $sql .= " AND p.ext_payment_site = '".$db->escape($paiement->ext_payment_site)."'";
497 $sql .= " AND p.fk_bank <> 0";
498 $result = $db->query($sql);
499 if ($result) {
500 if ($db->num_rows($result)) {
501 $ispaymentdone = 1;
502 $obj = $db->fetch_object($result);
503 dol_syslog('* Payment already linked to bank record '.$obj->fk_bank.' . We do not recreate the link');
504 }
505 }
506 if (!$ispaymentdone) {
507 dol_syslog('* Add payment to bank');
508
509 // The bank used is the one defined into Stripe setup
510 $paymentmethod = 'stripe';
511 $bankaccountid = getDolGlobalInt("STRIPE_BANK_ACCOUNT_FOR_PAYMENTS");
512
513 if ($bankaccountid > 0) {
514 $label = '(CustomerInvoicePayment)';
515 $result = $paiement->addPaymentToBank($user, 'payment', $label, $bankaccountid, $customer_id, '');
516 if ($result < 0) {
517 $postactionmessages[] = $paiement->error . ($paiement->error ? ' ' : '') . implode("<br>\n", $paiement->errors);
518 $ispostactionok = -1;
519 $error++;
520 } else {
521 $postactionmessages[] = 'Bank transaction of payment created (by ipn.php file)';
522 }
523 } else {
524 $postactionmessages[] = 'Setup of bank account to use in module ' . $paymentmethod . ' was not set. No way to record the payment.';
525 $ispostactionok = -1;
526 $error++;
527 }
528 }
529 }
530
531 if (!$error && isModEnabled('prelevement')) {
532 $bon = new BonPrelevement($db);
533 $idbon = 0;
534 $sql = "SELECT dp.fk_prelevement_bons as idbon";
535 $sql .= " FROM ".MAIN_DB_PREFIX."prelevement_demande as dp";
536 $sql .= " JOIN ".MAIN_DB_PREFIX."prelevement_bons as pb"; // Here we join to prevent modification of a prelevement bon already credited
537 $sql .= " ON pb.rowid = dp.fk_prelevement_bons";
538 $sql .= " WHERE dp.fk_facture = ".((int) $invoice_id);
539 $sql .= " AND dp.sourcetype = 'facture'";
540 $sql .= " AND dp.ext_payment_id = '".$db->escape($TRANSACTIONID)."'";
541 $sql .= " AND dp.traite = 1";
542 $sql .= " AND statut = ".((int) $bon::STATUS_TRANSFERED); // To be sure that it's not already credited
543 $result = $db->query($sql);
544 if ($result) {
545 if ($db->num_rows($result)) {
546 $obj = $db->fetch_object($result);
547 $idbon = $obj->idbon;
548 dol_syslog('* Prelevement must be set to credited');
549 } else {
550 dol_syslog('* Prelevement not found or already credited');
551 }
552 } else {
553 $postactionmessages[] = $db->lasterror();
554 $ispostactionok = -1;
555 $error++;
556 }
557
558 if (!$error && !empty($idbon)) {
559 $sql = "UPDATE ".MAIN_DB_PREFIX."prelevement_bons";
560 $sql .= " SET fk_user_credit = ".((int) $user->id);
561 $sql .= ", statut = ".((int) $bon::STATUS_CREDITED);
562 $sql .= ", date_credit = '".$db->idate($now)."'";
563 $sql .= ", credite = 1";
564 $sql .= " WHERE rowid = ".((int) $idbon);
565 $sql .= " AND statut = ".((int) $bon::STATUS_TRANSFERED);
566
567 $result = $db->query($sql);
568 if (!$result) {
569 $postactionmessages[] = $db->lasterror();
570 $ispostactionok = -1;
571 $error++;
572 }
573 }
574
575 if (!$error && !empty($idbon)) {
576 $sql = "UPDATE ".MAIN_DB_PREFIX."prelevement_lignes";
577 $sql .= " SET statut = 2";
578 $sql .= " WHERE fk_prelevement_bons = ".((int) $idbon);
579 $result = $db->query($sql);
580 if (!$result) {
581 $postactionmessages[] = $db->lasterror();
582 $ispostactionok = -1;
583 $error++;
584 }
585 }
586 }
587
588 if (!$error) {
589 $db->commit();
590 http_response_code(200);
591 return 1;
592 } else {
593 $db->rollback();
594 http_response_code(500);
595 return -1;
596 }
597 } else {
598 dol_syslog("The payment mode of this payment is ".$paymentTypeCode." in Stripe and ".$paymentTypeCodeInDolibarr." in Dolibarr. This case is not managed by the IPN");
599 }
600 } else {
601 dol_syslog("Nothing to do in database because we don't know paymentTypeIdInDolibarr");
602 }
603} elseif ($event->type == 'payment_intent.payment_failed') {
604 // When a try to take payment has failed. Useful for asynchronous SEPA payment that fails.
605 dol_syslog("A try to make a payment has failed");
606
607 $object = $event->data->object;
608 $ipaddress = $object->metadata->ipaddress;
609 $currencyCodeType = strtoupper($object->currency);
610 $paymentmethodstripeid = $object->payment_method;
611 $customer_id = $object->customer;
612
613 $chargesdataarray = array();
614 $objpayid = '';
615 $objpaydesc = '';
616 $objinvoiceid = 0;
617 $objerrcode = '';
618 $objerrmessage = '';
619 $objpaymentmodetype = '';
620 if (!empty($object->charges)) { // Old format
621 $chargesdataarray = $object->charges->data;
622 foreach ($chargesdataarray as $chargesdata) {
623 $objpayid = $chargesdata->id;
624 $objpaydesc = $chargesdata->description;
625 $objinvoiceid = 0;
626 if ($chargesdata->metadata->dol_type == 'facture') {
627 $objinvoiceid = $chargesdata->metadata->dol_id;
628 }
629 $objerrcode = $chargesdata->outcome->reason;
630 $objerrmessage = $chargesdata->outcome->seller_message;
631
632 $objpaymentmodetype = $chargesdata->payment_method_details->type;
633 break;
634 }
635 }
636 if (!empty($object->last_payment_error)) { // New format 2023-10-16
637 // $object is probably an object of type Stripe\PaymentIntent
638 $objpayid = $object->latest_charge;
639 $objpaydesc = $object->description;
640 $objinvoiceid = 0;
641 if ($object->metadata->dol_type == 'facture') {
642 $objinvoiceid = $object->metadata->dol_id;
643 }
644 $objerrcode = empty($object->last_payment_error->code) ? $object->last_payment_error->decline_code : $object->last_payment_error->code;
645 $objerrmessage = $object->last_payment_error->message;
646
647 $objpaymentmodetype = $object->last_payment_error->payment_method->type;
648 }
649
650 dol_syslog("objpayid=".$objpayid." objpaymentmodetype=".$objpaymentmodetype." objerrcode=".$objerrcode);
651
652 // If this is a differed payment for SEPA, add a line into agenda events
653 if ($objpaymentmodetype == 'sepa_debit') {
654 $db->begin();
655
656 require_once DOL_DOCUMENT_ROOT.'/comm/action/class/actioncomm.class.php';
657 $actioncomm = new ActionComm($db);
658
659 if ($objinvoiceid > 0) {
660 require_once DOL_DOCUMENT_ROOT.'/compta/facture/class/facture.class.php';
661 $invoice = new Facture($db);
662 $invoice->fetch($objinvoiceid);
663
664 $actioncomm->userownerid = 0;
665 $actioncomm->percentage = -1;
666
667 $actioncomm->type_code = 'AC_OTH_AUTO'; // Type of event ('AC_OTH', 'AC_OTH_AUTO', 'AC_XXX'...)
668 $actioncomm->code = 'AC_IPN';
669
670 $actioncomm->datep = $now;
671 $actioncomm->datef = $now;
672
673 $actioncomm->socid = $invoice->socid;
674 $actioncomm->fk_project = $invoice->fk_project;
675 $actioncomm->fk_element = $invoice->id;
676 $actioncomm->elementtype = 'invoice';
677 $actioncomm->ip = getUserRemoteIP();
678 }
679
680 $actioncomm->note_private = 'Error returned on payment id '.$objpayid.' after SEPA payment request '.$objpaydesc.'<br>Error code is: '.$objerrcode.'<br>Error message is: '.$objerrmessage;
681 $actioncomm->label = 'Payment error (SEPA Stripe)';
682
683 $result = $actioncomm->create($user);
684 if ($result <= 0) {
685 dol_syslog($actioncomm->error, LOG_ERR);
686 $error++;
687 }
688
689 if (! $error) {
690 $db->commit();
691 } else {
692 $db->rollback();
693 http_response_code(500);
694 return -1;
695 }
696 }
697} elseif ($event->type == 'checkout.session.completed') { // Called when making payment with new Checkout method ($conf->global->STRIPE_USE_NEW_CHECKOUT is on).
698 // TODO: create fees
699} elseif ($event->type == 'payment_method.attached') {
700 // When we link a payment method with a customer on Stripe side
701 require_once DOL_DOCUMENT_ROOT.'/societe/class/companypaymentmode.class.php';
702 require_once DOL_DOCUMENT_ROOT.'/societe/class/societeaccount.class.php';
703 $societeaccount = new SocieteAccount($db);
704
705 $companypaymentmode = new CompanyPaymentMode($db);
706
707 $idthirdparty = $societeaccount->getThirdPartyID($db->escape($event->data->object->customer), 'stripe', $servicestatus);
708 if ($idthirdparty > 0) {
709 // If the payment mode attached is to a stripe account owned by an external customer in societe_account (so a thirdparty that has a Stripe account),
710 // we can create the payment mode
711 $companypaymentmode->stripe_card_ref = $event->data->object->id;
712 $companypaymentmode->fk_soc = $idthirdparty;
713 $companypaymentmode->bank = null;
714 $companypaymentmode->label = '';
715 $companypaymentmode->number = $event->data->object->id;
716 $companypaymentmode->last_four = $event->data->object->card->last4;
717 $companypaymentmode->card_type = $event->data->object->card->branding;
718
719 $companypaymentmode->owner_name = $event->data->object->billing_details->name;
720 $companypaymentmode->proprio = $companypaymentmode->owner_name; // We may still need this formodulebuilder because name of field is "proprio"
721
722 $companypaymentmode->exp_date_month = (int) $event->data->object->card->exp_month;
723 $companypaymentmode->exp_date_year = (int) $event->data->object->card->exp_year;
724 $companypaymentmode->cvn = null;
725 $companypaymentmode->datec = $event->data->object->created;
726 $companypaymentmode->default_rib = 0;
727 $companypaymentmode->type = $event->data->object->type;
728 $companypaymentmode->country_code = $event->data->object->card->country;
729 $companypaymentmode->status = $servicestatus;
730
731 // TODO Check that a payment mode $companypaymentmode->stripe_card_ref does not exists yet to avoid to create duplicates
732 // so we can remove the test on STRIPE_NO_DUPLICATE_CHECK
733 if (getDolGlobalString('STRIPE_NO_DUPLICATE_CHECK')) {
734 $db->begin();
735 $result = $companypaymentmode->create($user);
736 if ($result < 0) {
737 $error++;
738 }
739 if (!$error) {
740 $db->commit();
741 } else {
742 $db->rollback();
743 http_response_code(500);
744 return -1;
745 }
746 }
747 }
748} elseif ($event->type == 'payment_method.updated') {
749 // When we update a payment method on Stripe side
750 require_once DOL_DOCUMENT_ROOT.'/societe/class/companypaymentmode.class.php';
751 $companypaymentmode = new CompanyPaymentMode($db);
752 $companypaymentmode->fetch(0, '', 0, '', " AND stripe_card_ref = '".$db->escape($event->data->object->id)."'");
753 if ($companypaymentmode->id > 0) {
754 // If we found a payment mode with the ID
755 $companypaymentmode->bank = null;
756 $companypaymentmode->label = '';
757 $companypaymentmode->number = $db->escape($event->data->object->id);
758 $companypaymentmode->last_four = $db->escape($event->data->object->card->last4);
759 $companypaymentmode->proprio = $db->escape($event->data->object->billing_details->name);
760 $companypaymentmode->exp_date_month = (int) $event->data->object->card->exp_month;
761 $companypaymentmode->exp_date_year = (int) $event->data->object->card->exp_year;
762 $companypaymentmode->cvn = null;
763 $companypaymentmode->datec = $db->escape($event->data->object->created);
764 $companypaymentmode->default_rib = 0;
765 $companypaymentmode->type = $db->escape($event->data->object->type);
766 $companypaymentmode->country_code = $db->escape($event->data->object->card->country);
767 $companypaymentmode->status = $servicestatus;
768
769 $db->begin();
770 if (!$error) {
771 $result = $companypaymentmode->update($user);
772 if ($result < 0) {
773 $error++;
774 }
775 }
776 if (!$error) {
777 $db->commit();
778 } else {
779 $db->rollback();
780 }
781 }
782} elseif ($event->type == 'payment_method.detached') {
783 // When we remove a payment method on Stripe side
784 $db->begin();
785 $sql = "DELETE FROM ".MAIN_DB_PREFIX."societe_rib WHERE number = '".$db->escape($event->data->object->id)."' and status = ".((int) $servicestatus);
786 $db->query($sql);
787 $db->commit();
788} elseif ($event->type == 'charge.succeeded') {
789 // Deprecated. TODO: create fees and redirect to paymentok.php
790} elseif ($event->type == 'charge.failed') {
791 // Deprecated. TODO: Redirect to paymentko.php
792} elseif (($event->type == 'source.chargeable') && ($event->data->object->type == 'three_d_secure') && ($event->data->object->three_d_secure->authenticated == true)) {
793 // Deprecated.
794} elseif ($event->type == 'charge.dispute.closed') {
795 // When a dispute to cancel a SEPA payment is finished
796 dol_syslog("object = ".var_export($event->data, true));
797} elseif ($event->type == 'charge.dispute.funds_withdrawn') {
798 // When a dispute/withdraw to cancel a SEPA payment is done
799 dol_syslog("object = ".var_export($event->data, true));
800
801 global $stripearrayofkeysbyenv;
802 $error = 0;
803 $errormsg = '';
804 $object = $event->data->object;
805 $TRANSACTIONID = $object->payment_intent;
806 $ipaddress = $object->metadata->ipaddress;
807 $now = dol_now();
808 $currencyCodeType = strtoupper($object->currency);
809 $paymentmethodstripeid = $object->payment_method;
810 $customer_id = $object->customer;
811 $reason = $object->reason;
812 $amountdisputestripe = $object->amoutndispute; // In stripe format
813 $amountdispute = $amountdisputestripe; // In real currency format
814
815 $invoice_id = "";
816 $paymentTypeCode = ""; // payment type according to Stripe
817 $paymentTypeCodeInDolibarr = ""; // payment type according to Dolibarr
818 $payment_amount = 0;
819 $payment_amountInDolibarr = 0;
820
821 dol_syslog("Try to find the payment in database for the payment_intent id = ".$TRANSACTIONID);
822
823 $sql = "SELECT pi.rowid, pi.fk_facture, pi.fk_prelevement_bons, pi.amount, pi.type, pi.traite";
824 $sql .= " FROM ".MAIN_DB_PREFIX."prelevement_demande as pi";
825 $sql .= " WHERE pi.ext_payment_id = '".$db->escape($TRANSACTIONID)."'";
826 $sql .= " AND pi.ext_payment_site = '".$db->escape($service)."'";
827
828 $result = $db->query($sql);
829 if ($result) {
830 $obj = $db->fetch_object($result);
831 if ($obj) {
832 if ($obj->type == 'ban') {
833 // This is a direct-debit with an order (llx_bon_prelevement).
834 $pdid = $obj->rowid;
835 $invoice_id = $obj->fk_facture;
836 $directdebitorcreditransfer_id = $obj->fk_prelevement_bons;
837 $payment_amountInDolibarr = $obj->amount;
838 $paymentTypeCodeInDolibarr = $obj->type;
839
840 dol_syslog("Found the payment intent for ban in database (pdid = ".$pdid." directdebitorcreditransfer_id=".$directdebitorcreditransfer_id.")");
841 }
842 if ($obj->type == 'card' || empty($obj->type)) {
843 // This is a card payment.
844 $pdid = $obj->rowid;
845 $invoice_id = $obj->fk_facture;
846 $payment_amountInDolibarr = $obj->amount;
847 $paymentTypeCodeInDolibarr = empty($obj->type) ? 'card' : $obj->type;
848
849 dol_syslog("Found the payment intent for card in database (pdid = ".$pdid." directdebitorcreditransfer_id=".$directdebitorcreditransfer_id.")");
850 }
851 } else {
852 dol_syslog("Payment intent ".$TRANSACTIONID." not found into database, so ignored.");
853 http_response_code(200);
854 print "Payment intent ".$TRANSACTIONID." not found into database, so ignored.";
855 return 1;
856 }
857 } else {
858 http_response_code(500);
859 print $db->lasterror();
860 return -1;
861 }
862
863 dol_syslog("objinvoiceid=".$invoice_id);
864 $tmpinvoice = new Facture($db);
865 $tmpinvoice->fetch($invoice_id);
866 $tmpinvoice->fetch_thirdparty();
867
868 dol_syslog("The payment disputed is ".$amountdispute." and the invoice is ".$payment_amountInDolibarr);
869
870 if ($amountdispute != $payment_amountInDolibarr) {
871 http_response_code(500);
872 print "The payment disputed is ".$amountdispute." and the invoice is ".$payment_amountInDolibarr.". Amount differs, we don't know what to do.";
873 return -1;
874 }
875
876 $accountfrom = new Account($db);
877 $accountfrom->fetch(getDolGlobalInt('STRIPE_BANK_ACCOUNT_FOR_PAYMENTS'));
878
879 // Now we add a negative payment
880 $paiement = new Paiement($db);
881
882 $amounts = array();
883 $amounts[$tmpinvoice->id] = -1 * $payment_amountInDolibarr;
884
885 $paiement->datepaye = dol_now();
886 $paiement->amounts = $amounts; // Array with all payments dispatching with invoice id
887 /*$paiement->multicurrency_amounts = $multicurrency_amounts; // Array with all payments dispatching
888 $paiement->multicurrency_code = $multicurrency_code; // Array with all currency of payments dispatching
889 $paiement->multicurrency_tx = $multicurrency_tx; // Array with all currency tx of payments dispatching
890 */
891 $paiement->paiementid = dol_getIdFromCode($db, 'PRE', 'c_paiement', 'code', 'id', 1);
892 $paiement->num_payment = $object->id; // A string like 'du_...'
893 $paiement->note_public = 'Fund withdrawn by bank. Reason: '.$reason;
894 $paiement->note_private = '';
895 $paiement->fk_account = $accountfrom->id;
896
897 $db->begin();
898
899 $alreadytransferedinaccounting = $tmpinvoice->getVentilExportCompta();
900
901 if ($alreadytransferedinaccounting) {
902 // TODO Test if invoice already in accountancy.
903 // What to do ?
904 $errormsg = 'Error: the invoice '.$tmpinvoice->id.' is already transferred into accounting. Don\'t know what to do.';
905 $error++;
906 }
907
908 if (! $error && $tmpinvoice->status == Facture::STATUS_CLOSED) {
909 // Switch back the invoice to status validated
910 $result = $tmpinvoice->setStatut(Facture::STATUS_VALIDATED);
911 if ($result < 0) {
912 $errormsg = $tmpinvoice->error.implode(', ', $tmpinvoice->errors);
913 $error++;
914 }
915 }
916
917 if (! $error) {
918 $paiement_id = $paiement->create($user, 0, $tmpinvoice->thirdparty); // This include regenerating documents
919 if ($paiement_id < 0) {
920 $errormsg = $paiement->error.implode(', ', $paiement->errors);
921 $error++;
922 }
923 }
924
925 if (!$error) {
926 //$db->commit(); // Code not yet enough tested
927 $db->rollback();
928 http_response_code(500);
929 return -1;
930 } else {
931 $db->rollback();
932 http_response_code(500);
933 print $errormsg;
934 return -1;
935 }
936}
937
938
939// End of page. Default return HTTP code will be 200
if( $user->socid > 0) if(! $user->hasRight('accounting', 'chartofaccount')) $object
Definition card.php:58
dolibarr_set_const($db, $name, $value, $type='chaine', $visible=0, $note='', $entity=1)
Insert a parameter (key,value) into database (delete old key then insert it again).
Class to manage bank accounts.
Class to manage agenda events (actions)
Class to manage withdrawal receipts.
Class to send emails (with attachments or not) Usage: $mailfile = new CMailFile($subject,...
Class for CompanyPaymentMode.
Class to manage invoices.
const STATUS_VALIDATED
Validated (need to be paid)
const STATUS_CLOSED
Classified paid.
Class to manage payments of customer invoices.
Class for SocieteAccount.
Stripe class @TODO No reason to extends CommonObject.
Class to manage Dolibarr users.
price2num($amount, $rounding='', $option=0)
Function that return a number with universal decimal format (decimal separator is '.
dolChmod($filepath, $newmask='')
Change mod of a file.
dol_now($mode='auto')
Return date for now.
getDolGlobalInt($key, $default=0)
Return a Dolibarr global constant int value.
dol_getIdFromCode($db, $key, $tablename, $fieldkey='code', $fieldid='id', $entityfilter=0, $filters='')
Return an id or code from a code or id.
dol_print_date($time, $format='', $tzoutput='auto', $outputlangs=null, $encodetooutput=false)
Output date in a string format according to outputlangs (or langs if not defined).
dolGetFirstLastname($firstname, $lastname, $nameorder=-1)
Return firstname and lastname in correct order.
getDolGlobalString($key, $default='')
Return a Dolibarr global constant string value.
getUserRemoteIP()
Return the IP of remote user.
dol_syslog($message, $level=LOG_INFO, $ident=0, $suffixinfilename='', $restricttologhandler='', $logcontext=null)
Write log message into outputs.
dol_escape_htmltag($stringtoescape, $keepb=0, $keepn=0, $noescapetags='', $escapeonlyhtmltags=0, $cleanalsojavascript=0)
Returns text escaped for inclusion in HTML alt or title or value tags, or into values of HTML input f...
if(!defined( 'NOREQUIREMENU')) if(!empty(GETPOST('seteventmessages', 'alpha'))) if(!function_exists("llxHeader")) top_httphead($contenttype='text/html', $forcenocache=0)
Show HTTP header.
httponly_accessforbidden($message='1', $http_response_code=403, $stringalreadysanitized=0)
Show a message to say access is forbidden and stop program.