dolibarr 25.0.0-alpha
ipn.php
1<?php
2/* Copyright (C) 2018-2020 Thibault FOUCART <support@ptibogxiv.net>
3 * Copyright (C) 2018-2026 Frédéric France <frederic.france@free.fr>
4 * Copyright (C) 2023 Laurent Destailleur <eldy@users.sourceforge.net>
5 * Copyright (C) 2024-2026 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';
54require_once DOL_DOCUMENT_ROOT.'/core/lib/admin.lib.php';
55require_once DOL_DOCUMENT_ROOT.'/user/class/user.class.php';
56require_once DOL_DOCUMENT_ROOT.'/core/class/ccountry.class.php';
57require_once DOL_DOCUMENT_ROOT.'/commande/class/commande.class.php';
58require_once DOL_DOCUMENT_ROOT.'/compta/paiement/class/paiement.class.php';
59require_once DOL_DOCUMENT_ROOT.'/compta/facture/class/facture.class.php';
60require_once DOL_DOCUMENT_ROOT.'/compta/bank/class/account.class.php';
61require_once DOL_DOCUMENT_ROOT.'/compta/prelevement/class/bonprelevement.class.php';
62require_once DOL_DOCUMENT_ROOT.'/societe/class/societe.class.php';
63require_once DOL_DOCUMENT_ROOT.'/core/class/CMailFile.class.php';
64require_once DOL_DOCUMENT_ROOT.'/includes/stripe/stripe-php/init.php';
65require_once DOL_DOCUMENT_ROOT.'/stripe/class/stripe.class.php';
66
67// You can find your endpoint's secret in your webhook settings
68if (GETPOSTISSET('connect')) {
69 if (GETPOSTISSET('test')) {
70 $endpoint_secret = getDolGlobalString('STRIPE_TEST_WEBHOOK_CONNECT_KEY');
71 $service = 'StripeTest';
72 $servicestatus = 0;
73 } else {
74 $endpoint_secret = getDolGlobalString('STRIPE_LIVE_WEBHOOK_CONNECT_KEY');
75 $service = 'StripeLive';
76 $servicestatus = 1;
77 }
78} else {
79 if (GETPOSTISSET('test')) {
80 $endpoint_secret = getDolGlobalString('STRIPE_TEST_WEBHOOK_KEY');
81 $service = 'StripeTest';
82 $servicestatus = 0;
83 } else {
84 $endpoint_secret = getDolGlobalString('STRIPE_LIVE_WEBHOOK_KEY');
85 $service = 'StripeLive';
86 $servicestatus = 1;
87 }
88}
89
90if (!isModEnabled('stripe')) {
91 httponly_accessforbidden('Module Stripe not enabled');
92}
93
94if (empty($endpoint_secret)) {
95 httponly_accessforbidden('Error: Setup of module Stripe not complete for mode '.dol_escape_htmltag($service).'. The WEBHOOK_KEY is not defined.', 400, 1);
96}
97
98if (getDolGlobalString('STRIPE_USER_ACCOUNT_FOR_ACTIONS')) {
99 // We set the user to use for all ipn actions in Dolibarr
100 $user = new User($db);
101 $user->fetch(getDolGlobalInt('STRIPE_USER_ACCOUNT_FOR_ACTIONS'));
102 $user->loadRights();
103} else {
104 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);
105}
106
107$now = dol_now();
108
109// Security
110// The test on security key is done later into constructEvent() method.
111
112
113/*
114 * Actions
115 */
116
117$payload = @file_get_contents("php://input");
118$sig_header = empty($_SERVER["HTTP_STRIPE_SIGNATURE"]) ? '' : $_SERVER["HTTP_STRIPE_SIGNATURE"];
119$event = null;
120
121if (getDolGlobalString('STRIPE_DEBUG')) {
122 $fh = fopen(DOL_DATA_ROOT.'/dolibarr_stripeipn_payload.log', 'w+');
123 if ($fh) {
124 fwrite($fh, dol_print_date(dol_now('gmt'), 'standard').' IPN Called. service='.$service.' HTTP_STRIPE_SIGNATURE='.$sig_header."\n");
125 fwrite($fh, $payload);
126 fclose($fh);
127 dolChmod(DOL_DATA_ROOT.'/dolibarr_stripeipn_payload.log');
128 }
129}
130
131$error = 0;
132
133try {
134 $event = \Stripe\Webhook::constructEvent($payload, $sig_header, $endpoint_secret);
135} catch (UnexpectedValueException $e) {
136 // Invalid payload
137 dol_syslog("***** Stripe IPN was called with UnexpectedValueException (invalid payload) service=".$service);
138 dol_syslog("***** Stripe IPN was called with UnexpectedValueException (invalid payload) service=".$service, LOG_DEBUG, 0, '_payment');
139 httponly_accessforbidden('Invalid payload', 400);
140} catch (\Stripe\Exception\SignatureVerificationException $e) {
141 dol_syslog("***** Stripe IPN was called with SignatureVerificationException service=".$service);
142 dol_syslog("***** Stripe IPN was called with SignatureVerificationException service=".$service, LOG_DEBUG, 0, '_payment');
143 httponly_accessforbidden('Invalid signature. May be a hook for an event created by another Stripe env or a hack attempt ? Check setup of your keys whsec_...', 400);
144} catch (Exception $e) { // @phpstan-ignore catch.neverThrown
145 dol_syslog("***** Stripe IPN was called with Exception (".$e->getMessage().") service=".$service);
146 dol_syslog("***** Stripe IPN was called with Exception (".$e->getMessage().") service=".$service, LOG_DEBUG, 0, '_payment');
147 httponly_accessforbidden('Error '.$e->getMessage(), 400);
148}
149
150// Do something with $event
151
152$langs->load("main");
153
154
155if (isModEnabled('multicompany') && !empty($conf->stripeconnect->enabled) && isset($mc) && is_object($mc)) {
156 $sql = "SELECT entity";
157 $sql .= " FROM ".MAIN_DB_PREFIX."oauth_token";
158 $sql .= " WHERE service = '".$db->escape($service)."' and tokenstring LIKE '%".$db->escape($db->escapeforlike($event->account))."%'";
159
160 dol_syslog(get_class($db)."::fetch", LOG_DEBUG, 0, '_payment');
161
162 $result = $db->query($sql);
163 if ($result) {
164 if ($db->num_rows($result)) {
165 $obj = $db->fetch_object($result);
166 $key = $obj->entity;
167 } else {
168 $key = 1;
169 }
170 } else {
171 $key = 1;
172 }
173 $ret = $mc->switchEntity($key);
174}
175
176$stripe = new Stripe($db);
177
178// Subject
179$societeName = getDolGlobalString('MAIN_INFO_SOCIETE_NOM');
180if (getDolGlobalString('MAIN_APPLICATION_TITLE')) {
181 $societeName = getDolGlobalString('MAIN_APPLICATION_TITLE');
182}
183
184
185// Add a delay to be sure that any Stripe action from webhooks are executed after interactive actions that also trigger a webhook
186sleep(2);
187
188
190
191dol_syslog("***** Stripe IPN was called with event->type=".$event->type." service=".$service);
192dol_syslog("***** Stripe IPN was called with event->type=".$event->type." service=".$service, LOG_DEBUG, 0, '_payment');
193
194
195// Hook to allow external modules to handle Stripe webhook events
196$hookmanager = new HookManager($db);
197$hookmanager->initHooks(array('stripeipn'));
198$parameters = array('event' => $event, 'servicestatus' => $servicestatus, 'service' => $service);
199$reshook = $hookmanager->executeHooks('stripeWebhookEvent', $parameters, $event->data->object, $event->type);
200if ($reshook > 0) {
201 // A hook handled the event entirely, stop processing
202 http_response_code(200);
203 return;
204}
205
206
207if ($event->type == 'payout.created' && getDolGlobalString('STRIPE_AUTO_RECORD_PAYOUT')) {
208 // When a payout is created by Stripe to transfer money to your account
209 dol_syslog("object = ".formatLogObject($event->data));
210 dol_syslog("object = ".formatLogObject($event->data), LOG_DEBUG, 0, '_payment');
211
212 $error = 0;
213
214 $result = dolibarr_set_const($db, $service."_NEXTPAYOUT", date('Y-m-d H:i:s', $event->data->object->arrival_date), 'chaine', 0, '', $conf->entity);
215
216 if ($result > 0) {
217 $subject = '['.$societeName.'] Notification - Stripe payout scheduled';
218 if (!empty($user->email)) {
219 $sendto = dolGetFirstLastname($user->firstname, $user->lastname)." <".$user->email.">";
220 } else {
221 $sendto = getDolGlobalString('MAIN_INFO_SOCIETE_MAIL') . '" <' . getDolGlobalString('MAIN_INFO_SOCIETE_MAIL').'>';
222 }
223 $replyto = $sendto;
224 $sendtocc = '';
225 if (getDolGlobalString('ONLINE_PAYMENT_SENDEMAIL')) {
226 $sendtocc = getDolGlobalString('ONLINE_PAYMENT_SENDEMAIL') . '" <' . getDolGlobalString('ONLINE_PAYMENT_SENDEMAIL').'>';
227 }
228
229 $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');
230
231 $mailfile = new CMailFile(
232 $subject,
233 $sendto,
234 $replyto,
235 $message,
236 array(),
237 array(),
238 array(),
239 $sendtocc,
240 '',
241 0,
242 -1
243 );
244
245 $ret = $mailfile->sendfile();
246
247 return 1;
248 } else {
249 $error++;
250 http_response_code(500);
251 return -1;
252 }
253} elseif ($event->type == 'payout.paid' && getDolGlobalString('STRIPE_AUTO_RECORD_PAYOUT')) {
254 // When a payout to transfer money to your account is completely done
255 dol_syslog("object = ".formatLogObject($event->data));
256 dol_syslog("object = ".formatLogObject($event->data), LOG_DEBUG, 0, '_payment');
257
258 $error = 0;
259 $result = dolibarr_set_const($db, $service."_NEXTPAYOUT", 0, 'chaine', 0, '', $conf->entity);
260 if ($result) {
261 $langs->load("errors");
262
263 $currency_code = getDolCurrency();
264
265 $dateo = dol_now();
266 $label = $event->data->object->description.' - '.getDolGlobalString('STRIPE_AUTO_RECORD_PAYOUT_LABEL', 'autorecord by IPN, see Stripe setup');
267 $amount = $stripe->convertAmount($event->data->object->amount, $currency_code, 1);
268
269 require_once DOL_DOCUMENT_ROOT.'/compta/bank/class/account.class.php';
270
271 $accountfrom = new Account($db);
272 $accountfrom->fetch(getDolGlobalInt('STRIPE_BANK_ACCOUNT_FOR_PAYMENTS'));
273
274 $accountto = new Account($db);
275 $accountto->fetch(getDolGlobalInt('STRIPE_BANK_ACCOUNT_FOR_BANKTRANSFERS'));
276
277 if (($accountto->id != $accountfrom->id) && empty($error)) {
278 $bank_line_id_from = 0;
279 $bank_line_id_to = 0;
280 $result = 0;
281
282 // By default, electronic transfer from bank to bank
283 $typefrom = 'PRE';
284 $typeto = 'VIR';
285
286 $numChqOrOpe = $event->data->object->id; // Store the payout reference (po_...) as the bank transfer number
287
288 $db->begin();
289
290 // Add entry into table llx_bank
291 $bank_line_id_from = $accountfrom->addline($dateo, $typefrom, $label, -1 * (float) price2num($amount), $numChqOrOpe, 0, $user, '', '', '', null, '', null, 'Record payout from public/stripe/ipn.php');
292
293 if (!($bank_line_id_from > 0)) {
294 $error++;
295 }
296 if (!$error) {
297 $bank_line_id_to = $accountto->addline($dateo, $typeto, $label, (float) price2num($amount), $numChqOrOpe, 0, $user, '', '', '', null, '', null, 'Record payout from public/stripe/ipn.php');
298 }
299 if (!($bank_line_id_to > 0)) {
300 $error++;
301 }
302
303 // Now add links of detail into llx_bank_url
304 if (!$error) {
305 $result = $accountfrom->add_url_line($bank_line_id_from, $bank_line_id_to, DOL_URL_ROOT.'/compta/bank/line.php?rowid=', '(banktransfert)', 'banktransfert');
306 }
307 if (!($result > 0)) {
308 $error++;
309 }
310 if (!$error) {
311 $result = $accountto->add_url_line($bank_line_id_to, $bank_line_id_from, DOL_URL_ROOT.'/compta/bank/line.php?rowid=', '(banktransfert)', 'banktransfert');
312 }
313 if (!($result > 0)) {
314 $error++;
315 }
316
317 if (!$error) {
318 $db->commit();
319 } else {
320 $db->rollback();
321 }
322
323 // Send email
324 if (!$error) {
325 $subject = '['.$societeName.'] Notification - Stripe payout done';
326 if (!empty($user->email)) {
327 $sendto = dolGetFirstLastname($user->firstname, $user->lastname)." <".$user->email.">";
328 } else {
329 $sendto = getDolGlobalString('MAIN_INFO_SOCIETE_MAIL');
330 }
331 $replyto = $sendto;
332 $sendtocc = '';
333 if (getDolGlobalString('ONLINE_PAYMENT_SENDEMAIL')) {
334 $sendtocc = getDolGlobalString('ONLINE_PAYMENT_SENDEMAIL');
335 }
336
337 $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');
338
339 $mailfile = new CMailFile(
340 $subject,
341 $sendto,
342 $replyto,
343 $message,
344 array(),
345 array(),
346 array(),
347 $sendtocc,
348 '',
349 0,
350 -1
351 );
352
353 $ret = $mailfile->sendfile();
354 }
355 }
356
357 return 1;
358 } else {
359 $error++;
360 http_response_code(500);
361 return -1;
362 }
363} elseif ($event->type == 'customer.source.created') {
364 //TODO: save customer's source
365} elseif ($event->type == 'customer.source.updated') {
366 //TODO: update customer's source
367} elseif ($event->type == 'customer.source.delete') {
368 //TODO: delete customer's source
369} elseif ($event->type == 'customer.deleted') {
370 // When a customer account is delete on Stripe side
371 $db->begin();
372 $sql = "DELETE FROM ".MAIN_DB_PREFIX."societe_account WHERE key_account = '".$db->escape($event->data->object->id)."' AND site = 'stripe'";
373 $db->query($sql);
374 $db->commit();
375} elseif ($event->type == 'payment_intent.succeeded') {
376 // Called when making payment with PaymentIntent method.
377 dol_syslog("object = ".formatLogObject($event->data));
378 dol_syslog("object = ".formatLogObject($event->data), LOG_DEBUG, 0, '_payment');
379
380 include_once DOL_DOCUMENT_ROOT . '/compta/paiement/class/paiement.class.php';
381 global $stripearrayofkeysbyenv;
382 $error = 0;
383 $object = $event->data->object;
384 $objectType = $object->metadata->dol_type;
385 $TRANSACTIONID = $object->id; // Example 'pi_123456789...'
386 $ipaddress = $object->metadata->ipaddress;
387 $remoteipaddress = getUserRemoteIP();
388 $now = dol_now();
389 $currencyCodeType = strtoupper($object->currency);
390 $paymentmethodstripeid = $object->payment_method;
391 $customer_id = $object->customer;
392 $invoice_id = 0;
393 $supplierinvoice_id = 0;
394 $salary_id = "";
395 $paymentTypeCode = ""; // payment type according to Stripe
396 $paymentTypeCodeInDolibarr = ""; // payment type according to Dolibarr
397 $payment_amount = 0;
398 $payment_amountInDolibarr = 0;
399
400 dol_syslog("Try to find a payment in database for the payment_intent id = ".$TRANSACTIONID);
401 dol_syslog("Try to find a payment in database for the payment_intent id = ".$TRANSACTIONID, LOG_DEBUG, 0, '_payment');
402
403 $sql = "SELECT pi.rowid, pi.fk_facture, fk_facture_fourn, fk_salary, pi.fk_prelevement_bons, pi.amount, pi.type, pi.traite";
404 $sql .= " FROM ".MAIN_DB_PREFIX."prelevement_demande as pi";
405 $sql .= " WHERE pi.ext_payment_id = '".$db->escape($TRANSACTIONID)."'";
406 $sql .= " AND pi.ext_payment_site = '".$db->escape($service)."'";
407
408 $result = $db->query($sql);
409 if ($result) {
410 $obj = $db->fetch_object($result);
411 if ($obj) {
412 if ($obj->type == 'ban') {
413 $pdid = $obj->rowid;
414 $directdebitorcreditransfer_id = $obj->fk_prelevement_bons;
415
416 if ($obj->traite == 1) {
417 // This is a direct-debit with an order (llx_bon_prelevement) ALREADY generated, so
418 // it means we received here the confirmation that payment request is finished.
419 $invoice_id = $obj->fk_facture;
420 $supplierinvoice_id = $obj->fk_facture_fourn;
421 $salary_id = $obj->fk_salary;
422 $payment_amountInDolibarr = $obj->amount;
423 $paymentTypeCodeInDolibarr = $obj->type;
424
425 dol_syslog("Found a request in database to pay with direct debit generated (pdid = ".$pdid." directdebitorcreditransfer_id=".$directdebitorcreditransfer_id.")");
426 dol_syslog("Found a request in database to pay with direct debit generated (pdid = ".$pdid." directdebitorcreditransfer_id=".$directdebitorcreditransfer_id.")", LOG_DEBUG, 0, '_payment');
427 } else {
428 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);
429 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, 0, '_payment');
430 }
431 }
432 if ($obj->type == 'card' || empty($obj->type)) {
433 $pdid = $obj->rowid;
434 if ($obj->traite == 0) {
435 // This is a card payment not already flagged as sent to Stripe.
436 $invoice_id = $obj->fk_facture;
437 $payment_amountInDolibarr = $obj->amount;
438 $paymentTypeCodeInDolibarr = empty($obj->type) ? 'card' : $obj->type;
439
440 dol_syslog("Found a request in database to pay with card (pdid = ".$pdid."). We should fix status traite to 1");
441 dol_syslog("Found a request in database to pay with card (pdid = ".$pdid."). We should fix status traite to 1", LOG_DEBUG, 0, '_payment');
442 } else {
443 dol_syslog("Found a request in database to pay with card (pdid = ".$pdid.") already set to traite=1. Nothing to fix.");
444 dol_syslog("Found a request in database to pay with card (pdid = ".$pdid.") already set to traite=1. Nothing to fix.", LOG_DEBUG, 0, '_payment');
445 }
446 }
447 } else {
448 dol_syslog("Payment intent ".$TRANSACTIONID." not found into database, so ignored.");
449 dol_syslog("Payment intent ".$TRANSACTIONID." not found into database, so ignored.", LOG_DEBUG, 0, '_payment');
450 http_response_code(200);
451 print "Payment intent ".$TRANSACTIONID." not found into database, so ignored.";
452 return 1;
453 }
454 } else {
455 http_response_code(500);
456 print $db->lasterror();
457 return -1;
458 }
459
460 if ($paymentTypeCodeInDolibarr) {
461 // Here, we need to do something. A $invoice_id has been found.
462
463 $stripeacc = $stripearrayofkeysbyenv[$servicestatus]['secret_key'];
464
465 dol_syslog("Get the Stripe payment object for the payment method id = ".formatLogObject($paymentmethodstripeid));
466 dol_syslog("Get the Stripe payment object for the payment method id = ".formatLogObject($paymentmethodstripeid), LOG_DEBUG, 0, '_payment');
467
468 $s = new \Stripe\StripeClient($stripeacc);
469
470 $paymentmethodstripe = $s->paymentMethods->retrieve($paymentmethodstripeid);
471 $paymentTypeCode = $paymentmethodstripe->type;
472 if ($paymentTypeCode == "ban" || $paymentTypeCode == "sepa_debit") {
473 $paymentTypeCode = "PRE";
474 } elseif ($paymentTypeCode == "card") {
475 $paymentTypeCode = "CB";
476 }
477
478 $payment_amount = $payment_amountInDolibarr;
479
480 // TODO Add this checks ? May not be required because the message is already decoded with $event = \Stripe\Webhook::constructEvent($payload, $sig_header, $endpoint_secret);
481 // - Check payment_amount in Stripe (received) is same than the one in Dolibarr
482 // - Check that payment intent is succeed (to avoid forged json webhook sent by malicious users)
483
484 $postactionmessages = array();
485
486 if ($paymentTypeCode == "CB" && ($paymentTypeCodeInDolibarr == 'card' || empty($paymentTypeCodeInDolibarr))) {
487 // Case payment type at Stripe side and into prelevement_demande are both CARD.
488 // For this case, payment should already have been recorded so we just update flag of payment request if not yet 1
489
490 // May be we should store py_... instead of pi_... but we started with pi_... so we continue.
491 $paiement_ext_payment_id = $TRANSACTIONID.':'.$customer_id.'@'.$stripearrayofkeysbyenv[$servicestatus]['publishable_key'];
492 $paiement_ext_payment_idold = $TRANSACTIONID;
493 $paiement_ext_payment_site = $service;
494
495 // Update the payment request to pay by credit card
496 $sql = "UPDATE ".MAIN_DB_PREFIX."prelevement_demande SET traite = 1 WHERE traite = 0";
497 $sql .= " AND (type = '' OR type = 'card')";
498 $sql .= " AND (ext_payment_id = '".$db->escape($paiement_ext_payment_id)."' OR ext_payment_id = '".$db->escape($paiement_ext_payment_idold)."')";
499 $sql .= " AND ext_payment_site = '".$db->escape($paiement_ext_payment_site)."'";
500 $sql .= " AND fk_facture = ".((int) $invoice_id);
501 $sql .= " AND sourcetype = 'facture'";
502
503 dol_syslog("TODO update flag traite to 1 sql=".$sql);
504 dol_syslog("TODO update flag traite to 1 sql=".$sql, LOG_DEBUG, 0, '_payment');
505 //$db->query($sql);
506 } elseif ($paymentTypeCode == "PRE" && $paymentTypeCodeInDolibarr == 'ban') {
507 // Case payment type is Direct Debit and into prelevement_demande is also BAN.
508 // For this case, payment on invoice (not yet recorded) must be recorded and direct debit order must be closed.
509
510 $paiement = new Paiement($db);
511
512 $paiement->datepaye = $now;
513 $paiement->date = $now;
514 if ($currencyCodeType == getDolCurrency()) {
515 $paiement->amounts = [$invoice_id => $payment_amount]; // Array with all payments dispatching with invoice id
516 } else {
517 $paiement->multicurrency_amounts = [$invoice_id => $payment_amount]; // Array with all payments dispatching
518
519 $postactionmessages[] = 'Payment was done in a currency ('.$currencyCodeType.') other than the expected currency of company ('.getDolCurrency().')';
520 $ispostactionok = -1;
521 // Not yet supported, so error
522 $error++;
523 }
524
525 // Get ID of payment PRE
526 $paiement->paiementcode = $paymentTypeCode;
527 $sql = "SELECT id FROM ".MAIN_DB_PREFIX."c_paiement";
528 $sql .= " WHERE code = '".$db->escape($paymentTypeCode)."'";
529 $sql .= " AND entity IN (".getEntity('c_paiement').")";
530 $resql = $db->query($sql);
531 if ($resql) {
532 $obj = $db->fetch_object($resql);
533 $paiement->paiementid = $obj->id;
534 } else {
535 $error++;
536 }
537
538 $paiement->num_payment = '';
539 $paiement->note_public = '';
540 $paiement->note_private = 'Stripe Sepa payment received by IPN service listening webhooks - ' . dol_print_date($now, 'standard') . ' (TZ server) using servicestatus=' . $servicestatus . ($remoteipaddress ? ' remote ip ' . $remoteipaddress : '').($ipaddress ? ' user ip ' . $ipaddress : '') . ' - Transaction ID = ' . $TRANSACTIONID;
541
542 $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.
543 $paiement->ext_payment_site = $service;
544
545 $ispaymentdone = 0;
546 $sql = "SELECT p.rowid FROM ".MAIN_DB_PREFIX."paiement as p";
547 $sql .= " WHERE (p.ext_payment_id = '".$db->escape($paiement->ext_payment_id)."' OR p.ext_payment_id = '".$db->escape($TRANSACTIONID)."')";
548 $sql .= " AND p.ext_payment_site = '".$db->escape($paiement->ext_payment_site)."'";
549 $result = $db->query($sql);
550 if ($result) {
551 if ($db->num_rows($result)) {
552 $ispaymentdone = 1;
553 dol_syslog('* Payment for ext_payment_id '.$paiement->ext_payment_id.' already done. We do not recreate the payment');
554 dol_syslog('* Payment for ext_payment_id '.$paiement->ext_payment_id.' already done. We do not recreate the payment', LOG_DEBUG, 0, '_payment');
555 }
556 }
557
558 $db->begin();
559
560 if (!$error && !$ispaymentdone) {
561 dol_syslog('* Record payment type PRE for invoice id ' . $invoice_id . '. It includes closing of invoice and regenerating document.');
562 dol_syslog('* Record payment type PRE for invoice id ' . $invoice_id . '. It includes closing of invoice and regenerating document.', LOG_DEBUG, 0, '_payment');
563
564 // This include closing invoices to 'paid' (and trigger including unsuspending) and regenerating document
565 $thirdpartyofpayment = null; // TODO Load thirdparty from $invoice_id
566
567 $paiement_id = $paiement->create($user, 1, $thirdpartyofpayment);
568 if ($paiement_id < 0) {
569 $postactionmessages[] = $paiement->error . ($paiement->error ? ' ' : '') . implode("<br>\n", $paiement->errors);
570 $ispostactionok = -1;
571 $error++;
572
573 dol_syslog("Failed to create the payment for invoice id " . $invoice_id);
574 dol_syslog("Failed to create the payment for invoice id " . $invoice_id, LOG_DEBUG, 0, '_payment');
575 } else {
576 $postactionmessages[] = 'Payment created';
577
578 dol_syslog("The payment has been created for invoice id " . $invoice_id);
579 dol_syslog("The payment has been created for invoice id " . $invoice_id, LOG_DEBUG, 0, '_payment');
580 }
581 }
582
583 if (!$error && isModEnabled('bank')) {
584 // 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).
585 $ispaymentdone = 0;
586 $sql = "SELECT p.rowid, p.fk_bank FROM ".MAIN_DB_PREFIX."paiement as p";
587 $sql .= " WHERE (p.ext_payment_id = '".$db->escape($paiement->ext_payment_id)."' OR p.ext_payment_id = '".$db->escape($TRANSACTIONID)."')";
588 $sql .= " AND p.ext_payment_site = '".$db->escape($paiement->ext_payment_site)."'";
589 $sql .= " AND p.fk_bank <> 0";
590 $result = $db->query($sql);
591 if ($result) {
592 if ($db->num_rows($result)) {
593 $ispaymentdone = 1;
594 $obj = $db->fetch_object($result);
595 dol_syslog('* Payment already linked to bank record '.$obj->fk_bank.' . We do not recreate the link');
596 dol_syslog('* Payment already linked to bank record '.$obj->fk_bank.' . We do not recreate the link', LOG_DEBUG, 0, '_payment');
597 }
598 }
599 if (!$ispaymentdone) {
600 dol_syslog('* Add payment to bank');
601 dol_syslog('* Add payment to bank', LOG_DEBUG, 0, '_payment');
602
603 // The bank used is the one defined into Stripe setup
604 $paymentmethod = 'stripe';
605 $bankaccountid = getDolGlobalInt("STRIPE_BANK_ACCOUNT_FOR_PAYMENTS");
606
607 if ($bankaccountid > 0) {
608 $label = '(CustomerInvoicePayment)';
609 $result = $paiement->addPaymentToBank($user, 'payment', $label, $bankaccountid, $customer_id, '');
610 if ($result < 0) {
611 $postactionmessages[] = $paiement->error . ($paiement->error ? ' ' : '') . implode("<br>\n", $paiement->errors);
612 $ispostactionok = -1;
613 $error++;
614 } else {
615 $postactionmessages[] = 'Bank transaction of payment created (by ipn.php file)';
616 }
617 } else {
618 $postactionmessages[] = 'Setup of bank account to use in module ' . $paymentmethod . ' was not set. No way to record the payment.';
619 $ispostactionok = -1;
620 $error++;
621 }
622 }
623 }
624
625 if (!$error && isModEnabled('prelevement')) {
626 $bon = new BonPrelevement($db);
627 $idbon = 0;
628 $sql = "SELECT dp.fk_prelevement_bons as idbon";
629 $sql .= " FROM ".MAIN_DB_PREFIX."prelevement_demande as dp";
630 $sql .= " JOIN ".MAIN_DB_PREFIX."prelevement_bons as pb"; // Here we join to prevent modification of a prelevement bon already credited
631 $sql .= " ON pb.rowid = dp.fk_prelevement_bons";
632 $sql .= " WHERE dp.fk_facture = ".((int) $invoice_id);
633 $sql .= " AND dp.sourcetype = 'facture'";
634 $sql .= " AND (dp.ext_payment_id = '".$db->escape($paiement->ext_payment_id)."' OR dp.ext_payment_id = '".$db->escape($TRANSACTIONID)."')";
635 $sql .= " AND dp.traite = 1";
636 $sql .= " AND statut = ".((int) $bon::STATUS_TRANSFERED); // To be sure that it's not already credited
637 $result = $db->query($sql);
638 if ($result) {
639 if ($db->num_rows($result)) {
640 $obj = $db->fetch_object($result);
641 $idbon = $obj->idbon;
642 dol_syslog('* Prelevement must be set to credited');
643 dol_syslog('* Prelevement must be set to credited', LOG_DEBUG, 0, '_payment');
644 } else {
645 dol_syslog('* Prelevement not found or already credited');
646 dol_syslog('* Prelevement not found or already credited', LOG_DEBUG, 0, '_payment');
647 }
648 } else {
649 $postactionmessages[] = $db->lasterror();
650 $ispostactionok = -1;
651 $error++;
652 }
653
654 if (!$error && !empty($idbon)) {
655 $sql = "UPDATE ".MAIN_DB_PREFIX."prelevement_bons";
656 $sql .= " SET fk_user_credit = ".((int) $user->id);
657 $sql .= ", statut = ".((int) $bon::STATUS_CREDITED);
658 $sql .= ", date_credit = '".$db->idate($now)."'";
659 $sql .= ", credite = 1";
660 $sql .= " WHERE rowid = ".((int) $idbon);
661 $sql .= " AND statut = ".((int) $bon::STATUS_TRANSFERED);
662
663 $result = $db->query($sql);
664 if (!$result) {
665 $postactionmessages[] = $db->lasterror();
666 $ispostactionok = -1;
667 $error++;
668 }
669 }
670
671 if (!$error && !empty($idbon)) {
672 $sql = "UPDATE ".MAIN_DB_PREFIX."prelevement_lignes";
673 $sql .= " SET statut = 2";
674 $sql .= " WHERE fk_prelevement_bons = ".((int) $idbon);
675 $result = $db->query($sql);
676 if (!$result) {
677 $postactionmessages[] = $db->lasterror();
678 $ispostactionok = -1;
679 $error++;
680 }
681 }
682 }
683
684 if (!$error) {
685 $db->commit();
686 } else {
687 $db->rollback();
688 http_response_code(500);
689 return -1;
690 }
691
692 if (getDolGlobalString('STRIPE_IPN_SEND_EMAIL_ON_DIRECT_DEBIT_CONFIRMATION')) {
693 $db->begin();
694
695 // If option to send email after confirmation of direct debit is on, we send the email (template must exists)
696 $labeltouse = getDolGlobalString('STRIPE_IPN_SEND_EMAIL_ON_DIRECT_DEBIT_CONFIRMATION');
697 // Example: $labeltouse = 'InvoicePaymentSuccess'
698
699 $invoice = new Facture($db);
700 $invoice->fetch($invoice_id);
701 $invoice->fetch_thirdparty();
702
703 // Set output language
704 $outputlangs = new Translate('', $conf);
705 $outputlangs->setDefaultLang(empty($invoice->thirdparty->default_lang) ? $mysoc->default_lang : $invoice->thirdparty->default_lang);
706 $outputlangs->loadLangs(array("main", "members", "bills"));
707
708 // Get email content from template
709 $arraydefaultmessage = null;
710
711 include_once DOL_DOCUMENT_ROOT.'/core/class/html.formmail.class.php';
712 $formmail = new FormMail($db);
713
714 $arraydefaultmessage = $formmail->getEMailTemplate($db, 'facture_send', $user, $outputlangs, 0, 1, $labeltouse);
715
716 $appli = $mysoc->name;
717
718 // Default value
719 $subject = '['.$appli.'] Invoice direct debit payment recieved';
720 $msg = 'An invoice direct debit payment for invoice '.$invoice->ref.' has been recieved';
721 if (is_object($arraydefaultmessage) && $arraydefaultmessage->id > 0) { // We overwrite with value from template
722 $subject = $arraydefaultmessage->topic;
723 $msg = $arraydefaultmessage->content;
724 }
725
726 $substitutionarray = getCommonSubstitutionArray($outputlangs, 0, null, $invoice);
727
728 complete_substitutions_array($substitutionarray, $outputlangs, $object);
729
730 // Set the property ->ref_customer with ref_customer of contract so __REF_CLIENT__ will be replaced in email content
731 // Search contract linked to invoice
732 $foundcontract = null;
733 $invoice->fetchObjectLinked(null, '', null, '', 'OR', 1, 'sourcetype', 1);
734
735 if (is_array($invoice->linkedObjects['contrat']) && count($invoice->linkedObjects['contrat']) > 0) {
736 //dol_sort_array($object->linkedObjects['facture'], 'date');
737 foreach ($invoice->linkedObjects['contrat'] as $contract) {
739 '@phan-var-force Contrat $contract';
740 $substitutionarray['__CONTRACT_REF__'] = $contract->ref_customer;
741 $substitutionarray['__REFCLIENT__'] = $contract->ref_customer; // For backward compatibility
742 $substitutionarray['__REF_CLIENT__'] = $contract->ref_customer;
743 $substitutionarray['__REF_CUSTOMER__'] = $contract->ref_customer;
744 $foundcontract = $contract;
745 break;
746 }
747 }
748
749 dol_syslog('__DIRECTDOWNLOAD_URL_INVOICE__='.$substitutionarray['__DIRECTDOWNLOAD_URL_INVOICE__']);
750
751 $subjecttosend = make_substitutions($subject, $substitutionarray, $outputlangs);
752 $texttosend = make_substitutions($msg, $substitutionarray, $outputlangs);
753
754 // Attach a file ?
755 $listofpaths = array();
756 $listofnames = array();
757 $listofmimes = array();
758
759 /*
760 $invoicediroutput = $conf->invoice->dir_output;
761 $fileparams = dol_most_recent_file($invoicediroutput . '/' . $invoice->ref, preg_quote($invoice->ref, '/').'[^\-]+');
762 $file = $fileparams['fullname'];
763 $file = ''; // Disable attachment of invoice in emails
764
765 if ($file) {
766 $listofpaths=array($file);
767 $listofnames=array(basename($file));
768 $listofmimes=array(dol_mimetype($file));
769 }
770 */
771
772 $from = getDolGlobalString('MAIN_INFO_SOCIETE_MAIL');
773
774 $trackid = 'inv'.$invoice->id;
775 $moreinheader = 'X-Dolibarr-Info: public stripe ipn.php'."\r\n";
776 $addr_cc = '';
777 if (!empty($invoice->thirdparty->array_options['options_emailccinvoice'])) {
778 dol_syslog("We add the recipient ".$invoice->thirdparty->array_options['options_emailccinvoice']." as CC", LOG_DEBUG);
779 $addr_cc = $invoice->thirdparty->array_options['options_emailccinvoice'];
780 }
781
782 // Send email (substitutionarray must be done just before this)
783 include_once DOL_DOCUMENT_ROOT.'/core/class/CMailFile.class.php';
784 $mailfile = new CMailFile($subjecttosend, $invoice->thirdparty->email, $from, $texttosend, $listofpaths, $listofmimes, $listofnames, $addr_cc, '', 0, -1, '', '', $trackid, $moreinheader);
785 if (empty($mailfile->error) && $mailfile->sendfile()) {
786 $result = 1;
787
788 dol_syslog("Option STRIPE_IPN_SEND_EMAIL_ON_DIRECT_DEBIT_CONFIRMATION: Email sent");
789 dol_syslog("Option STRIPE_IPN_SEND_EMAIL_ON_DIRECT_DEBIT_CONFIRMATION: Email sent", LOG_DEBUG, 0, '_payment');
790 } else {
791 $errmsg = 'Option STRIPE_IPN_SEND_EMAIL_ON_DIRECT_DEBIT_CONFIRMATION: '.$langs->trans("ErrorFailedToSendMail", $from, $invoice->thirdparty->email).'. '.$mailfile->error;
792
793 dol_syslog($errmsg);
794 dol_syslog($errmsg, LOG_WARNING, 0, '_payment');
795 }
796
797 $db->commit();
798 } else {
799 dol_syslog("Option STRIPE_IPN_SEND_EMAIL_ON_DIRECT_DEBIT_CONFIRMATION not set to the template label. No email sent.");
800 dol_syslog("Option STRIPE_IPN_SEND_EMAIL_ON_DIRECT_DEBIT_CONFIRMATION not set to the template label. No email sent.", LOG_DEBUG, 0, '_payment');
801 }
802 } else {
803 dol_syslog("The payment mode of this payment is ".$paymentTypeCode." in Stripe and ".$paymentTypeCodeInDolibarr." in Dolibarr. This case is not managed by the IPN");
804 dol_syslog("The payment mode of this payment is ".$paymentTypeCode." in Stripe and ".$paymentTypeCodeInDolibarr." in Dolibarr. This case is not managed by the IPN", LOG_DEBUG, 0, '_payment');
805 }
806 } else {
807 dol_syslog("Nothing to do in database because we don't know paymentTypeIdInDolibarr");
808 dol_syslog("Nothing to do in database because we don't know paymentTypeIdInDolibarr", LOG_DEBUG, 0, '_payment');
809 }
810} elseif ($event->type == 'payment_intent.payment_failed') {
811 // When a try to take payment has failed. Useful for asynchronous SEPA payment that fails.
812 dol_syslog("A try to make a payment has failed");
813 dol_syslog("A try to make a payment has failed", LOG_DEBUG, 0, '_payment');
814
815 $object = $event->data->object;
816 $ipaddress = $object->metadata->ipaddress;
817 $remoteipaddress = getUserRemoteIP();
818 $currencyCodeType = strtoupper($object->currency);
819 $paymentmethodstripeid = $object->payment_method;
820 $customer_id = $object->customer;
821
822 $chargesdataarray = array();
823 $objpayid = '';
824 $objpaydesc = '';
825 $objinvoiceid = 0;
826 $objerrcode = '';
827 $objerrmessage = '';
828 $objpaymentmodetype = '';
829 if (!empty($object->charges)) { // Old format
830 $chargesdataarray = $object->charges->data;
831 foreach ($chargesdataarray as $chargesdata) {
832 $objpayid = $chargesdata->id;
833 $objpaydesc = $chargesdata->description;
834 $objinvoiceid = 0;
835 if ($chargesdata->metadata->dol_type == 'facture') {
836 $objinvoiceid = $chargesdata->metadata->dol_id;
837 }
838 $objerrcode = $chargesdata->outcome->reason;
839 $objerrmessage = $chargesdata->outcome->seller_message;
840
841 $objpaymentmodetype = $chargesdata->payment_method_details->type;
842 break;
843 }
844 }
845 if (!empty($object->last_payment_error)) { // New format 2023-10-16
846 // $object is probably an object of type Stripe\PaymentIntent
847 $objpayid = $object->latest_charge;
848 $objpaydesc = $object->description;
849 $objinvoiceid = 0;
850 if ($object->metadata->dol_type == 'facture') {
851 $objinvoiceid = $object->metadata->dol_id;
852 }
853 $objerrcode = empty($object->last_payment_error->code) ? $object->last_payment_error->decline_code : $object->last_payment_error->code;
854 $objerrmessage = $object->last_payment_error->message;
855
856 $objpaymentmodetype = $object->last_payment_error->payment_method->type;
857 }
858
859 dol_syslog("objpayid=".$objpayid." objpaymentmodetype=".$objpaymentmodetype." objerrcode=".$objerrcode);
860 dol_syslog("objpayid=".$objpayid." objpaymentmodetype=".$objpaymentmodetype." objerrcode=".$objerrcode, LOG_DEBUG, 0, '_payment');
861
862 // If this is a differed payment for SEPA, add a line into agenda events
863 if ($objpaymentmodetype == 'sepa_debit') {
864 $db->begin();
865
866 require_once DOL_DOCUMENT_ROOT.'/comm/action/class/actioncomm.class.php';
867 $actioncomm = new ActionComm($db);
868
869 if ($objinvoiceid > 0) {
870 require_once DOL_DOCUMENT_ROOT.'/compta/facture/class/facture.class.php';
871 $invoice = new Facture($db);
872 $invoice->fetch($objinvoiceid);
873
874 $actioncomm->userownerid = 0;
875 $actioncomm->percentage = -1;
876
877 $actioncomm->type_code = 'AC_OTH_AUTO'; // Type of event ('AC_OTH', 'AC_OTH_AUTO', 'AC_XXX'...)
878 $actioncomm->code = 'AC_PAYMENT_STRIPE_IPN_SEPA_KO';
879
880 $actioncomm->datep = $now;
881 $actioncomm->datef = $now;
882
883 $actioncomm->socid = $invoice->socid;
884 $actioncomm->fk_project = $invoice->fk_project;
885 $actioncomm->elementid = $invoice->id;
886 $actioncomm->elementtype = 'invoice';
887 $actioncomm->ip = getUserRemoteIP();
888 }
889
890 $actioncomm->note_private = 'Stripe Sepa payment error received by IPN service listening webhooks - ' . dol_print_date($now, 'standard') . ' (TZ server) using servicestatus=' . $servicestatus . ($remoteipaddress ? ' remote ip ' . $remoteipaddress : '').($ipaddress ? ' user ip ' . $ipaddress : '').' - Payment id '.$objpayid.' after SEPA payment request '.$objpaydesc.'<br>Error code is: '.$objerrcode.'<br>Error message is: '.$objerrmessage;
891 $actioncomm->label = 'Payment error (SEPA Stripe)';
892
893 $result = $actioncomm->create($user);
894 if ($result <= 0) {
895 dol_syslog($actioncomm->error, LOG_ERR);
896 dol_syslog($actioncomm->error, LOG_ERR, 0, '_payment');
897 $error++;
898 }
899
900 if (! $error) {
901 // Option: Force status of invoice to "dispute on"
902 //$invoice->dispute_status = 1;
903 //$invoice->update($user);
904
905 // Option: Sen email to customer
906 // TODO
907
908 $db->commit();
909 } else {
910 $db->rollback();
911 http_response_code(500);
912 return -1;
913 }
914 }
915} elseif ($event->type == 'checkout.session.completed') { // Called when making payment with new Checkout method (getDolGlobalString('STRIPE_USE_NEW_CHECKOUT') is on).
916 // TODO: create fees
917} elseif ($event->type == 'payment_method.attached') {
918 dol_syslog("object = ".formatLogObject($event->data));
919 dol_syslog("object = ".formatLogObject($event->data), LOG_DEBUG, 0, '_payment');
920
921 // When we link a payment method with a customer on Stripe side
922 require_once DOL_DOCUMENT_ROOT.'/societe/class/companypaymentmode.class.php';
923 require_once DOL_DOCUMENT_ROOT.'/societe/class/societeaccount.class.php';
924 $societeaccount = new SocieteAccount($db);
925
926 $companypaymentmode = new CompanyPaymentMode($db);
927
928 $idthirdparty = $societeaccount->getThirdPartyID($db->escape($event->data->object->customer), 'stripe', $servicestatus);
929 if ($idthirdparty > 0) {
930 // 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),
931 // we can create the payment mode
932 $companypaymentmode->stripe_card_ref = $event->data->object->id;
933 $companypaymentmode->fk_soc = $idthirdparty;
934 $companypaymentmode->bank = null;
935 $companypaymentmode->label = '';
936 $companypaymentmode->number = $event->data->object->id;
937 $companypaymentmode->last_four = $event->data->object->card->last4;
938 $companypaymentmode->card_type = $event->data->object->card->branding;
939
940 $companypaymentmode->owner_name = $event->data->object->billing_details->name;
941 $companypaymentmode->proprio = $companypaymentmode->owner_name; // We may still need this for modulebuilder code because name of field is "proprio"
942
943 $companypaymentmode->exp_date_month = (int) $event->data->object->card->exp_month;
944 $companypaymentmode->exp_date_year = (int) $event->data->object->card->exp_year;
945 $companypaymentmode->cvn = null;
946 $companypaymentmode->datec = $event->data->object->created;
947 $companypaymentmode->default_rib = 0;
948 $companypaymentmode->type = $event->data->object->type;
949 $companypaymentmode->country_code = $event->data->object->card->country;
950 $companypaymentmode->status = $servicestatus;
951
952 // TODO Check that a payment mode $companypaymentmode->stripe_card_ref does not exists yet to avoid to create duplicates
953 // so we can remove the test on STRIPE_NO_DUPLICATE_CHECK
954 if (getDolGlobalString('STRIPE_NO_DUPLICATE_CHECK')) {
955 $db->begin();
956 $result = $companypaymentmode->create($user);
957 if ($result < 0) {
958 $error++;
959 }
960 if (!$error) {
961 $db->commit();
962 } else {
963 $db->rollback();
964 http_response_code(500);
965 return -1;
966 }
967 }
968 }
969} elseif ($event->type == 'payment_method.updated') {
970 dol_syslog("object = ".formatLogObject($event->data));
971 dol_syslog("object = ".formatLogObject($event->data), LOG_DEBUG, 0, '_payment');
972
973 // When we update a payment method on Stripe side
974 require_once DOL_DOCUMENT_ROOT.'/societe/class/companypaymentmode.class.php';
975 $companypaymentmode = new CompanyPaymentMode($db);
976 $companypaymentmode->fetch(0, '', 0, '', " AND stripe_card_ref = '".$db->escape($event->data->object->id)."'");
977 if ($companypaymentmode->id > 0) {
978 // If we found a payment mode with the ID
979 $companypaymentmode->bank = null;
980 $companypaymentmode->label = '';
981 $companypaymentmode->number = $db->escape($event->data->object->id);
982 $companypaymentmode->last_four = $db->escape($event->data->object->card->last4);
983 $companypaymentmode->proprio = $db->escape($event->data->object->billing_details->name); // deprecated but still needed
984 $companypaymentmode->owner_name = $db->escape($event->data->object->billing_details->name);
985 $companypaymentmode->exp_date_month = (int) $event->data->object->card->exp_month;
986 $companypaymentmode->exp_date_year = (int) $event->data->object->card->exp_year;
987 $companypaymentmode->cvn = null;
988 $companypaymentmode->datec = (int) $event->data->object->created;
989 $companypaymentmode->default_rib = 0;
990 $companypaymentmode->type = $db->escape($event->data->object->type);
991 $companypaymentmode->country_code = $db->escape($event->data->object->card->country);
992 $companypaymentmode->status = $servicestatus;
993
994 $db->begin();
995
996 $result = $companypaymentmode->update($user);
997 if ($result < 0) {
998 $error++;
999 }
1000
1001 if (!$error) {
1002 $db->commit();
1003 } else {
1004 $db->rollback();
1005
1006 http_response_code(500);
1007 return -1;
1008 }
1009 }
1010} elseif ($event->type == 'payment_method.detached') {
1011 // When we remove a payment method on Stripe side
1012 $db->begin();
1013 $sql = "DELETE FROM ".MAIN_DB_PREFIX."societe_rib WHERE number = '".$db->escape($event->data->object->id)."' and status = ".((int) $servicestatus);
1014 $db->query($sql);
1015 $db->commit();
1016} elseif ($event->type == 'charge.succeeded') {
1017 // Deprecated. TODO: create fees and redirect to paymentok.php
1018} elseif ($event->type == 'charge.failed') {
1019 // Deprecated. TODO: Redirect to paymentko.php
1020} elseif (($event->type == 'source.chargeable') && ($event->data->object->type == 'three_d_secure') && ($event->data->object->three_d_secure->authenticated == true)) {
1021 // Deprecated.
1022} elseif ($event->type == 'charge.dispute.closed') {
1023 // When a dispute to cancel a SEPA payment is finished
1024 dol_syslog("object = ".formatLogObject($event->data));
1025 dol_syslog("object = ".formatLogObject($event->data), LOG_DEBUG, 0, '_payment');
1026} elseif ($event->type == 'charge.dispute.funds_withdrawn') {
1027 // When a dispute/withdraw to cancel a payment (card or SEPA) is done
1028 dol_syslog("object = ".formatLogObject($event->data));
1029 dol_syslog("object = ".formatLogObject($event->data), LOG_DEBUG, 0, '_payment');
1030
1031 global $stripearrayofkeysbyenv;
1032 $error = 0;
1033 $errormsg = '';
1034 $object = $event->data->object;
1035 $TRANSACTIONID = $object->payment_intent;
1036 $ipaddress = $object->metadata->ipaddress;
1037 $remoteipaddress = getUserRemoteIP();
1038 $now = dol_now();
1039 $currencyCodeType = strtoupper($object->currency);
1040 $paymentmethodstripeid = $object->payment_method;
1041 $customer_id = $object->customer;
1042 $reason = $object->reason;
1043 $amountdisputestripe = $object->amount; // In stripe format
1044 $amountdispute = $stripe->convertAmount($amountdisputestripe, $currencyCodeType, 1); // In real currency format
1045 $statusdispute = $object->status;
1046
1047 $pkey = '';
1048 if (isset($stripearrayofkeysbyenv[$servicestatus]['publishable_key'])) {
1049 $pkey = $stripearrayofkeysbyenv[$servicestatus]['publishable_key'];
1050 }
1051 $LONGTRANSACTIONID = $TRANSACTIONID.':'.$customer_id.'@'.$pkey;
1052
1053 // Get the amount of fees for the dispute
1054 $balance_transactions_array = $object->balance_transactions;
1055 $feesstripe = 0;
1056 if (!empty($balance_transactions_array) && is_array($balance_transactions_array)) {
1057 foreach ($balance_transactions_array as $tmpval) {
1058 if (isset($tmpval['fee'])) {
1059 $feesstripe += (int) $tmpval['fee']; // In stripe format
1060 }
1061 }
1062 }
1063 $fees = $stripe->convertAmount($feesstripe, $currencyCodeType, 1); // In real currency format
1064
1065 $invoice_id = 0;
1066 $paymentTypeCode = ""; // payment type according to Stripe
1067 $paymentTypeCodeInDolibarr = ""; // payment type according to Dolibarr
1068 $payment_amount = 0;
1069 $payment_amountInDolibarr = 0;
1070
1071 dol_syslog("Try to find the payment in database for the payment_intent id = ".$TRANSACTIONID);
1072 dol_syslog("Try to find the payment in database for the payment_intent id = ".$TRANSACTIONID, LOG_DEBUG, 0, '_payment');
1073
1074 $sql = "SELECT pi.rowid, pi.fk_facture, pi.fk_prelevement_bons, pi.amount, pi.type, pi.traite";
1075 $sql .= " FROM ".MAIN_DB_PREFIX."prelevement_demande as pi";
1076 //$sql .= " WHERE pi.ext_payment_id = '".$db->escape($TRANSACTIONID)."'";
1077 $sql .= " WHERE (pi.ext_payment_id = '".$db->escape($LONGTRANSACTIONID)."' OR pi.ext_payment_id = '".$db->escape($TRANSACTIONID)."')";
1078 $sql .= " AND pi.ext_payment_site = '".$db->escape($service)."'";
1079
1080 $result = $db->query($sql);
1081 if ($result) {
1082 $obj = $db->fetch_object($result);
1083 if ($obj) {
1084 if ($obj->type == 'ban') {
1085 // This is a direct-debit with an order (llx_bon_prelevement).
1086 $pdid = $obj->rowid;
1087 $invoice_id = $obj->fk_facture;
1088 $directdebitorcreditransfer_id = $obj->fk_prelevement_bons;
1089 $payment_amountInDolibarr = $obj->amount;
1090 $paymentTypeCodeInDolibarr = $obj->type;
1091
1092 dol_syslog("Found the payment intent for ban in database (pdid = ".$pdid." directdebitorcreditransfer_id=".$directdebitorcreditransfer_id.")");
1093 dol_syslog("Found the payment intent for ban in database (pdid = ".$pdid." directdebitorcreditransfer_id=".$directdebitorcreditransfer_id.")", LOG_DEBUG, 0, '_payment');
1094 }
1095 if ($obj->type == 'card' || empty($obj->type)) {
1096 // This is a card payment.
1097 $pdid = $obj->rowid;
1098 $invoice_id = $obj->fk_facture;
1099 $directdebitorcreditransfer_id = 0;
1100 $payment_amountInDolibarr = $obj->amount;
1101 $paymentTypeCodeInDolibarr = empty($obj->type) ? 'card' : $obj->type;
1102
1103 dol_syslog("Found the payment intent for card in database (pdid = ".$pdid." directdebitorcreditransfer_id=".$directdebitorcreditransfer_id.")");
1104 dol_syslog("Found the payment intent for card in database (pdid = ".$pdid." directdebitorcreditransfer_id=".$directdebitorcreditransfer_id.")", LOG_DEBUG, 0, '_payment');
1105 }
1106 } else {
1107 dol_syslog("Payment intent ".$TRANSACTIONID." not found into database, so ignored.");
1108 dol_syslog("Payment intent ".$TRANSACTIONID." not found into database, so ignored.", LOG_DEBUG, 0, '_payment');
1109 http_response_code(200);
1110 print "Payment intent ".$TRANSACTIONID." not found into database, so ignored.";
1111 return 1;
1112 }
1113 } else {
1114 http_response_code(500);
1115 print $db->lasterror();
1116 return -1;
1117 }
1118
1119 dol_syslog("objinvoiceid=".$invoice_id);
1120 dol_syslog("objinvoiceid=".$invoice_id, LOG_DEBUG, 0, '_payment');
1121 $tmpinvoice = new Facture($db);
1122 $tmpinvoice->fetch($invoice_id);
1123 $tmpinvoice->fetch_thirdparty();
1124
1125 dol_syslog("The payment disputed has the amount ".$amountdispute.", fees of ".$fees." and the invoice has ".$payment_amountInDolibarr);
1126 dol_syslog("The payment disputed has the amount ".$amountdispute.", fees of ".$fees." and the invoice has ".$payment_amountInDolibarr, LOG_DEBUG, 0, '_payment');
1127
1128 // Amount may differ: sometimes amount for chargback is higher or lower than initial amount. No explanation (may be currencyrate ?)
1129 // So we disable this protection
1130 /*
1131 if ($amountdispute != $payment_amountInDolibarr) {
1132 http_response_code(500);
1133 print "The payment disputed has the amount ".$amountdispute." and the invoice has ".$payment_amountInDolibarr.". Amount is too different, we don't know what to do.";
1134 dol_syslog("Amount differs, we don't know what to do - Return HTTP 500.", LOG_WARNING, 0, '_payment');
1135 http_response_code(500);
1136 return -1;
1137 }
1138 */
1139
1140 if ($statusdispute == 'needs_response') {
1141 // Payment is disputed, but not yet refunded.
1142 $db->begin();
1143
1144 // If invoice was closed, we reopen it
1145 if ($tmpinvoice->status == Facture::STATUS_CLOSED) {
1146 // Switch back the invoice to status validated
1147 $result = $tmpinvoice->setStatut(Facture::STATUS_VALIDATED, null, '', 'none'); // Trigger will be run later
1148 if ($result < 0) {
1149 $errormsg = $tmpinvoice->error.implode(', ', $tmpinvoice->errors);
1150 $error++;
1151 }
1152 }
1153
1154 /* disabled, a record should already be done with the invoice update
1155 $actioncomm = new ActionComm($db);
1156 $actioncode = 'OTHER';
1157
1158 $actioncomm->type_code = 'AC_OTH_AUTO'; // Type of event ('AC_OTH', 'AC_OTH_AUTO', 'AC_XXX'...)
1159 $actioncomm->code = 'AC_'.$actioncode;
1160 $actioncomm->label = 'Payment dispute has been received by Stripe';
1161 $actioncomm->note_private = 'Payment dispute has been received by Stripe';
1162 $actioncomm->fk_project = 0;
1163 $actioncomm->datep = $now;
1164 $actioncomm->datef = $now;
1165 $actioncomm->percentage = -1; // Not applicable
1166 $actioncomm->socid = $tmpinvoice->thirdparty->id;
1167 $actioncomm->contact_id = 0;
1168 $actioncomm->authorid = $user->id; // User saving action
1169 $actioncomm->userownerid = $user->id; // Owner of action
1170
1171 $actioncomm->elementid = $tmpinvoice->id;
1172 $actioncomm->elementtype = $tmpinvoice->element;
1173
1174 $actioncomm->create($user);
1175 */
1176
1177 // Add a flag "dispute_status" in invoice table to Dispute Open
1178 $result = $tmpinvoice->setStatut(Facture::STATUS_VALIDATED, null, '', 'FACTURE_MODIFY', 'dispute_status');
1179 if ($result < 0) {
1180 $errormsg = $tmpinvoice->error.implode(', ', $tmpinvoice->errors);
1181 $error++;
1182 }
1183
1184 if (!$error) {
1185 $db->commit();
1186
1187 dol_syslog("The dispute_status of invoice ".$tmpinvoice->ref." has been modified to 1");
1188 dol_syslog("The dispute_status of invoice ".$tmpinvoice->ref." has been modified to 1", LOG_DEBUG, 0, '_payment');
1189
1190 http_response_code(200);
1191 print "Payment dispute received for ".$TRANSACTIONID.". We have changed the status of dispute_status to 1 for invoice ".$tmpinvoice->ref;
1192 return 1;
1193 } else {
1194 $db->rollback();
1195
1196 dol_syslog("Technicalerror ".$db->lasterror()." - ".$errormsg, LOG_ERR);
1197 dol_syslog("Technicalerror ".$db->lasterror()." - ".$errormsg, LOG_ERR, 0, '_payment');
1198
1199 http_response_code(500);
1200 print $db->lasterror();
1201 return -1;
1202 }
1203 } else {
1204 // Payment dispute is confirmed and refunded.
1205 $accountfrom = new Account($db);
1206 $accountfrom->fetch(getDolGlobalInt('STRIPE_BANK_ACCOUNT_FOR_PAYMENTS'));
1207
1208 // Now we add a negative payment
1209 $paiement = new Paiement($db);
1210
1211 $amounts = array();
1212 $amounts[$tmpinvoice->id] = -1 * $payment_amountInDolibarr;
1213
1214 $paiement->datepaye = dol_now();
1215 $paiement->amounts = $amounts; // Array with all payments dispatching with invoice id
1216 /*$paiement->multicurrency_amounts = $multicurrency_amounts; // Array with all payments dispatching
1217 $paiement->multicurrency_code = $multicurrency_code; // Array with all currency of payments dispatching
1218 $paiement->multicurrency_tx = $multicurrency_tx; // Array with all currency tx of payments dispatching
1219 */
1220 $paiement->paiementid = dol_getIdFromCode($db, 'PRE', 'c_paiement', 'code', 'id', 1);
1221 $paiement->num_payment = $object->id; // A string like 'du_...'
1222
1223 $paiement->note_private = 'Stripe fund withdrawn message received by IPN service listening webhooks - ' . dol_print_date($now, 'standard') . ' (TZ server) using servicestatus=' . $servicestatus . ($remoteipaddress ? ' remote ip ' . $remoteipaddress : '').($ipaddress ? ' user ip ' . $ipaddress : '');
1224 $paiement->note_private .= ' - Fund withdrawn by bank with id='.$object->id.'. Reason: '.$reason.'. A fee of '.$fees.' may have been charged by Stripe.';
1225
1226 $paiement->fk_account = $accountfrom->id;
1227
1228 $paiement->ext_payment_id = $object->payment_intent;
1229 $paiement->ext_payment_site = $service;
1230
1231 $db->begin();
1232
1233 $alreadytransferedinaccounting = $tmpinvoice->getVentilExportCompta();
1234
1235 dol_syslog("The invoice has alreadytransferedinaccounting=".$alreadytransferedinaccounting);
1236 dol_syslog("The invoice has alreadytransferedinaccounting=".$alreadytransferedinaccounting, LOG_DEBUG, 0, '_payment');
1237
1238 /*
1239 if ($alreadytransferedinaccounting) {
1240 // TODO Test if invoice already in accountancy.
1241 // If yes, what to do ?
1242 $errormsg = 'Error: the invoice '.$tmpinvoice->id.' is already transferred into accounting. Don\'t know what to do.';
1243 $error++;
1244 }
1245 */
1246
1247 if (!$error && !$alreadytransferedinaccounting && $tmpinvoice->status == Facture::STATUS_CLOSED) {
1248 // Switch back the invoice to status validated
1249 $result = $tmpinvoice->setStatut(Facture::STATUS_VALIDATED, null, '', 'none');
1250 if ($result < 0) {
1251 $errormsg = $tmpinvoice->error.implode(', ', $tmpinvoice->errors);
1252 $error++;
1253 }
1254 }
1255
1256 if (!$error) {
1257 // Add status dispute_status to Dispute Open
1258 $result = $tmpinvoice->setStatut(Facture::STATUS_VALIDATED, null, '', 'FACTURE_MODIFY', 'dispute_status');
1259 if ($result < 0) {
1260 $errormsg = $tmpinvoice->error.implode(', ', $tmpinvoice->errors);
1261 $error++;
1262 }
1263
1264 if (!$error) {
1265 dol_syslog("The dispute_status of invoice ".$tmpinvoice->ref." has been modified to 1");
1266 dol_syslog("The dispute_status of invoice ".$tmpinvoice->ref." has been modified to 1", LOG_DEBUG, 0, '_payment');
1267 }
1268 }
1269
1270 // Check that a withdrawn payment does not already exists for the withdrawn (if IPN is sent twice by Stripe)
1271 $withdrawn_payment_already_exists = true; // By default, we assume that it exists
1272 $sql = "SELECT p.rowid, p.ref";
1273 $sql .= " FROM ".MAIN_DB_PREFIX."paiement as p";
1274 $sql .= " WHERE p.ext_payment_id = '".$db->escape($paiement->ext_payment_id)."'";
1275 $sql .= " AND p.ext_payment_site = '".$db->escape($service)."'";
1276
1277 $tmpresql = $db->query($sql);
1278 if ($tmpresql) {
1279 $obj = $db->fetch_object($tmpresql);
1280 if (empty($obj)) {
1281 $withdrawn_payment_already_exists = false;
1282 dol_syslog("No withdraw payment already exists", LOG_DEBUG);
1283 } else {
1284 dol_syslog("A withdraw payment already exists", LOG_DEBUG);
1285 }
1286 }
1287
1288 if (!$error && !$alreadytransferedinaccounting && !$withdrawn_payment_already_exists) {
1289 if ($paiement->fk_account > 0) {
1290 // If not yet in accountnacy, we can record the negative payment, otherwise, only the dispute status will be set and user
1291 // will have to make manual correction like a credit note.
1292 dol_syslog("We try to record the payment");
1293 dol_syslog("We try to record the payment", LOG_DEBUG, 0, '_payment');
1294
1295 $paiement_id = $paiement->create($user, 0, $tmpinvoice->thirdparty); // This include regenerating documents
1296 if ($paiement_id < 0) {
1297 $errormsg = $paiement->error.implode(', ', $paiement->errors);
1298 $error++;
1299 } else {
1300 $banklineid = $paiement->addPaymentToBank($user, 'payment', 'IPN Stripe dispute funds withdrawn', $paiement->fk_account, '', '', 1, '', '');
1301 if ($banklineid < 0) {
1302 $errormsg = $paiement->error.implode(', ', $paiement->errors);
1303 $error++;
1304 }
1305 }
1306 } else {
1307 dol_syslog("No bank account defined to record payment so no payment recorded");
1308 dol_syslog("No bank account defined to record payment so no payment recorded", LOG_DEBUG, 0, '_payment');
1309 }
1310 }
1311
1312 if (!$error) {
1313 // TODO
1314 // Record a payment for Stripe fees ?
1315 }
1316
1317 if (!$error) {
1318 $db->commit();
1319 //$db->rollback();
1320 //http_response_code(500);
1321
1322 dol_syslog("Invoice status updated and/or Revert payment created", LOG_WARNING);
1323 dol_syslog("Invoice status updated and/or Revert payment created", LOG_WARNING, 0, '_payment');
1324 } else {
1325 $db->rollback();
1326
1327 dol_syslog("Error - Return HTTP 500 - ".$errormsg, LOG_ERR);
1328 dol_syslog("Error - Return HTTP 500 - ".$errormsg, LOG_ERR, 0, '_payment');
1329
1330 http_response_code(500);
1331
1332 print $errormsg;
1333 return -1;
1334 }
1335 }
1336}
1337
1338
1339// End of page. Default return HTTP code will be 200
if(! $sortfield) if(! $sortorder) $object
Definition account.php:100
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 a HTML form to send a unitary email Usage: $formail = new FormMail($db) $formmail->pr...
Class to manage hooks.
Class to manage payments of customer invoices.
Class for SocieteAccount.
Stripe class @TODO No reason to extend CommonObject.
Class to manage translations.
Class to manage Dolibarr users.
global $mysoc
if(!isModEnabled('ai')||!getDolGlobalString('AI_ASSISTANT_ENABLED')) global $conf
The main.inc.php has been included so the following variable are now defined:
if(!isModEnabled('ai')||!getDolGlobalString('AI_ASSISTANT_ENABLED')) global $db
API class for accounts.
dol_now($mode='gmt')
Return date for now.
dol_getIdFromCode($db, $key, $tablename, $fieldkey='code', $fieldid='id', $entityfilter=0, $filters='', $useCache=true)
Return an id or code from a code or id.
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.
getDolGlobalInt($key, $default=0)
Return a Dolibarr global constant int value.
dolGetFirstLastname($firstname, $lastname, $nameorder=-1)
Return firstname and lastname in correct order.
getDolCurrency()
Return the main currency ('EUR', 'USD', ...)
complete_substitutions_array(&$substitutionarray, $outputlangs, $object=null, $parameters=null, $callfunc="completesubstitutionarray")
Complete the $substitutionarray with more entries coming from external module that had set the "subst...
make_substitutions($text, $substitutionarray, $outputlangs=null, $converttextinhtmlifnecessary=0)
Make substitution into a text string, replacing keys with vals from $substitutionarray (oldval=>newva...
if(!function_exists( 'utf8_encode')) if(!function_exists('utf8_decode')) if(!function_exists( 'str_starts_with')) if(!function_exists('str_ends_with')) if(!function_exists( 'str_contains')) formatLogObject($data)
Return a string serialized to be output on log with dol_syslog() An option allow to output log in one...
dol_print_date($time, $format='', $tzoutput='auto', $outputlangs=null, $encodetooutput=false, $decorate=0)
Output date in a string format according to outputlangs (or langs if not defined).
getUserRemoteIP($trusted=0)
Return the real IP of remote user.
getDolGlobalString($key, $default='')
Return a Dolibarr global constant string value.
isModEnabled($module)
Is Dolibarr module enabled.
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...
Definition html.lib.php:172
if(!defined( 'NOREQUIREMENU')) if(!empty(GETPOST('seteventmessages', 'alpha'))) if(!function_exists("llxHeader")) top_httphead($contenttype='text/html', $forcenocache=0)
Show HTTP header.
print $langs trans('Date')." left Ref Label right Qty right Price right TotalHT right TotalTTC right right right right right right right right right centpercent right TotalHT right n right VAT right n right TotalVAT right n No sujeto a RE IRPF right TotalLT1 right n right TotalLT2 right n right TotalTTC right n takeposcustomercurrency takeposcustomercurrency takeposcustomercurrency takeposcustomercurrency right TotalTTC takeposcustomercurrency right takeposcustomercurrency n right Paid right PaymentTypeShortLIQ right SELECT p pos_change as p datep as date
Definition receipt.php:487
httponly_accessforbidden($message='1', $http_response_code=403, $stringalreadysanitized=0)
Show a message to say access is forbidden and stop program.