PluginProbe
Pay with Vipps and MobilePay for WooCommerce / 6.1.8
Pay with Vipps and MobilePay for WooCommerce v6.1.8
6.2.1 6.2.0 6.1.10 6.1.9 6.1.8 6.1.7 6.1.6 6.1.5 6.1.4 6.1.3 6.1.2 6.1.1 6.1.0 6.0.5 6.0.4 6.0.3 6.0.2 6.0.1 6.0.0 5.4.3 5.4.2 5.4.1 5.4.0 5.3.4 trunk All 183 releases
woo-vipps / payment / VippsApi.class.php

VippsApi.class.php in Pay with Vipps and MobilePay for WooCommerce 6.1.8, at payment/VippsApi.class.php

1,473 lines 69.6 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /*
3 This is the VippsApi class, a delegate in WC_Payment_Gateway that handles the actual communication with Vipps.
4 The parameters are fetched from the containing class. IOK 2018-05-11
5
6
7 This file is part of the plugin Pay with Vipps and MobilePay for WooCommerce
8 Copyright (c) 2019 WP-Hosting AS
9
10 MIT License
11
12 Copyright (c) 2019 WP-Hosting AS
13
14 Permission is hereby granted, free of charge, to any person obtaining a copy
15 of this software and associated documentation files (the "Software"), to deal
16 in the Software without restriction, including without limitation the rights
17 to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
18 copies of the Software, and to permit persons to whom the Software is
19 furnished to do so, subject to the following conditions:
20
21 The above copyright notice and this permission notice shall be included in all
22 copies or substantial portions of the Software.
23
24 THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
25 IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
26 FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
27 AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
28 LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
29 OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
30 SOFTWARE.
31
32
33 */
34 if ( ! defined( 'ABSPATH' ) ) {
35 exit; // Exit if accessed directly
36 }
37 require_once(dirname(__FILE__) . "/VippsAPIException.class.php");
38
39 class VippsApi {
40 public $gateway;
41
42 public function __construct($gateway) {
43 $this->gateway = $gateway;
44 }
45
46 // These abstraction gets the correct client id and so forth based on whether or not test mode is on
47 public function get_merchant_serial() {
48 return $this->gateway->get_merchant_serial();
49 }
50 public function get_clientid($msn="") {
51 return $this->gateway->get_clientid($msn);
52 }
53 public function get_secret($msn="") {
54 return $this->gateway->get_secret($msn);
55 }
56 public function get_key($msn="") {
57 return $this->gateway->get_key($msn);
58 }
59 // Orderprefix is the same for all MSN (currently)
60 public function get_orderprefix() {
61 return $this->gateway->get_orderprefix();
62 }
63
64 public function get_option($optionname) {
65 return $this->gateway->get_option($optionname);
66 }
67 public function log($what,$type='info') {
68 return $this->gateway->log($what,$type);
69 }
70
71 // All methods get these headers, adding meta info, access token etc
72 public function get_headers($msn="") {
73 if (!$msn) $msn =$this->get_merchant_serial();
74 $date = gmdate('c');
75 $ip = $_SERVER['SERVER_ADDR'] ?? $_SERVER['LOCAL_ADDR'] ?? '127.0.0.1' ;
76 $at = $this->get_access_token($msn);
77 $subkey = $this->get_key($msn);
78
79 if (!$msn || !$at || !$subkey) {
80 return null;
81 }
82
83 $headers = array();
84 $headers['Authorization'] = 'Bearer ' . $at;
85 $headers['X-TimeStamp'] = $date;
86 $headers['X-Source-Address'] = $ip;
87 $headers['Ocp-Apim-Subscription-Key'] = $subkey;
88 $headers['Merchant-Serial-Number'] = $msn;
89
90 $headers['Vipps-System-Name'] = 'woocommerce';
91 $headers['Vipps-System-Version'] = get_bloginfo( 'version' ) . "/" . WC_VERSION;
92 $headers['Vipps-System-Plugin-Name'] = 'woo-vipps';
93 $headers['Vipps-System-Plugin-Version'] = WOO_VIPPS_VERSION;
94 return $headers;
95 }
96
97 // List all webhooks registered for the given MSN IOK 2023-12-19
98 // will return false on error and empty when no webhooks. Use the "raw" version to get
99 // the errors. 2025-01-31
100 public function get_webhooks($msn="") {
101 try {
102 return $this->get_webhooks_raw($msn);
103 return $res;
104 } catch (Exception $e) {
105 $this->log(sprintf(__("Could not get webhooks for merchant serial number %1\$s: ", 'woo-vipps'), $msn) . $e->getMessage(), 'error');
106 return false;
107 }
108 }
109
110 // version of get webhooks that re-throws errors so you can detect configuration problems easier
111 // IOK 2025-01-31
112 public function get_webhooks_raw($msn="") {
113 $command = "webhooks/v1/webhooks";
114 if (!$msn) $msn = $this->get_merchant_serial();
115 $headers = $this->get_headers($msn);
116 $args = [];
117 $res = $this->http_call($msn,$command,$args,'GET',$headers,'json');
118 return $res;
119 }
120
121
122 // Try to register a webhook for the site and the MSN passed
123 public function register_webhook($msn, $callback, $events=null) {
124 $command = "webhooks/v1/webhooks";
125 if (!$msn) $msn = $this->get_merchant_serial();
126 $headers = $this->get_headers($msn);
127 // We want the authorized event, and all the "no longer relevant" events. IOK 2023-12-19
128 if (!$events) {
129 $events = ['epayments.payment.authorized.v1', 'epayments.payment.aborted.v1', 'epayments.payment.expired.v1', 'epayments.payment.terminated.v1'];
130 }
131 $args = ['url'=>$callback, 'events'=>$events];
132 try {
133 $res = $this->http_call($msn,$command,$args,'POST',$headers,'json');
134 return $res;
135 } catch (Exception $e) {
136 $this->log(sprintf(__("Could not register webhooks for merchant serial number %1\$s callback %2\$s: ", 'woo-vipps'), $msn, $callback) . $e->getMessage(), 'error');
137 return false;
138 }
139 }
140 // Delete a webhook with the given id
141 public function delete_webhook($msn, $id) {
142 $command = "webhooks/v1/webhooks/" . $id;
143 if (!$msn) $msn = $this->get_merchant_serial();
144 $headers = $this->get_headers($msn);
145 $args = [];
146 try {
147 $res = $this->http_call($msn,$command,$args,'DELETE',$headers,'json');
148 return $res;
149 } catch (Exception $e) {
150 $this->log(sprintf(__("Could not delete webhook for merchant serial number %1\$s id %2\$s: ", 'woo-vipps'), $msn, $id) . $e->getMessage(), 'error');
151 return false;
152 }
153 }
154
155 // Get an App access token if neccesary. Returns this or throws an error. IOK 2018-04-18
156 // IOK 2023-12-19 changed this so the system could use several MSNs in the future.
157 public function get_access_token($msn="",$force=0) {
158 $msn = $msn ?? $this->get_merchant_serial();
159 $transientname = '_vipps_app_token' . '_' . sanitize_title($msn);
160 // First, get a stored token if it exists
161 $stored = get_transient($transientname);
162 if (!$force && $stored && $stored['expires_on'] > time()) {
163 return $stored['access_token'];
164 }
165 // Otherwise, get it from vipps - this might throw errors
166 $fresh = $this->get_access_token_from_vipps($msn);
167 if (!$fresh) return null;
168
169 $at = $fresh['access_token'];
170 $expire = $fresh['expires_in']/2;
171 set_transient($transientname,$fresh,$expire);
172 return $at;
173 }
174
175 // Fetch an access token if possible from the Vipps Api IOK 2018-04-18
176 private function get_access_token_from_vipps($msn="") {
177 $clientid=$this->get_clientid($msn);
178 $secret=$this->get_secret($msn);
179 $subkey = $this->get_key($msn);
180
181 $command = 'accessToken/get';
182 try {
183 $args = array('client_id'=>$clientid,'client_secret'=>$secret,'Ocp-Apim-Subscription-Key'=>$subkey);
184 $result = $this->http_call($msn,$command,array(),'POST',$args,'url');
185 return $result;
186 } catch (TemporaryVippsAPIException $e) {
187 $this->log(__("Could not get Vipps access token",'woo-vipps') .' '. $e->getMessage(), 'error');
188 throw $e;
189 } catch (Exception $e) {
190 $this->log(__("Could not get Vipps access token",'woo-vipps') .' '. $e->getMessage(). "\n" . $e->getMessage(), 'error');
191 throw new VippsAPIConfigurationException($e->getMessage());
192 }
193 }
194
195 # Order Management API functions
196 // 200, 400, 401, 404, 409, 409 invalid params also
197 public function add_image ($image, $is_bytes=false){
198 $command = 'order-management/v1/images/';
199 $bytes = $image;
200
201
202 if (!$is_bytes){
203 if ($image && is_readable($image)) {
204 $bytes = file_get_contents($image);
205 if (!$bytes) {
206 $this->log(__("Could not read image file: ",'woo-vipps') .' '. $image, 'error');
207 return false;
208 }
209
210 // Check image dimensions
211 $imageinfo = getimagesize($image);
212 if ($imageinfo && $imageinfo[1] < 167) {
213 $this->log(__("Image height too small - minimum height requirement is 167px", 'woo-vipps'), 'error');
214 return false;
215 }
216 }
217 }
218
219 $msn = $this->get_merchant_serial();
220 $headers = $this->get_headers($msn);
221
222 // imageid = ^[0-9A-Za-z-_\.] - 128 chars
223 $imageid = hash('sha512',$bytes); // Yields 128 hex chars
224 $base64 = base64_encode($bytes);
225 $args = ['imageId'=>$imageid,'src'=>$base64,'type'=>'base64'];
226
227 try {
228 $res = $this->http_call($msn,$command,$args,'POST',$headers,'json');
229 return $res['imageId'];
230 } catch (Exception $e) {
231 if ($this->is_duplicate_error($e)) return $imageid;
232
233 if ($this->is_image_size_error($e)) {
234 $this->log(__("Image rejected by Vipps - minimum height requirement is 167px", 'woo-vipps'), 'error');
235 return false;
236 }
237
238 $this->log(__("Could not send image to Vipps: ", 'woo-vipps') . $e->getMessage(), 'error');
239 return false;
240 }
241 }
242
243 private function is_duplicate_error($e) {
244 if (!is_a($e, 'VippsApiException')) return false;
245 return ($e->responsecode == 409) ||
246 ($e->responsecode == 400 && preg_match("!duplicate!i", $e->getMessage()));
247 }
248
249 private function is_image_size_error($e) {
250 if (!is_a($e, 'VippsApiException')) return false;
251 return $e->responsecode == 400 &&
252 (strpos($e->getMessage(), 'height') !== false ||
253 strpos($e->getMessage(), 'size') !== false);
254 }
255
256 // Used by the add_receipt API call as well as epayment_initiate_payment. The latter will use this
257 // if we have the shipping information - that is, we are using the normal woo checkout flow. IOK 2023-12-13
258 public function get_receipt_data($order) {
259 $receiptdata = [];
260 try {
261 $orderlines = [];
262 $bottomline = ['tipAmount'=>0, 'giftCardAmount'=>0, 'terminalId'=>'woocommerce'];
263 $bottomline['currency'] = $order->get_currency();
264
265 // 'receipt.bottomLine.giftCardAmount' is deprecated in epayment: https://developer.vippsmobilepay.com/api/epayment/#tag/CreatePayments
266 // and same for 'bottomLine.giftCardAmount' in order management api: https://developer.vippsmobilepay.com/api/order-management/#tag/Orderlines/operation/post-receipt-v2
267 // LP 2025-07-10
268 $giftcardamount = apply_filters('woo_vipps_order_giftcard_amount', 0, $order);
269 $tipamount = apply_filters('woo_vipps_order_tip_amount', 0, $order);
270 $bottomline['tipAmount'] = round($tipamount*100);
271 $bottomline['giftCardAmount'] = round($giftcardamount*100);
272
273 // 'receipt.bottomLine.terminalId' is deprecated in epayment: https://developer.vippsmobilepay.com/api/epayment/#tag/CreatePayments/operation/createPayment
274 // and same for 'bottomLine.terminalId' in order management api: https://developer.vippsmobilepay.com/api/order-management/#tag/Orderlines/operation/post-receipt-v2
275 // ----> use 'posId' instead.
276 // LP 2025-07-10
277 $bottomline['terminalId'] = apply_filters('woo_vipps_order_terminalid', 'woocommerce', $order);
278 $bottomline['receiptNumber'] = strval($order->get_id());
279
280 foreach ($order->get_items() as $key => $order_item) {
281 $orderline = [];
282 $prodid = $order_item->get_product_id(); // sku can be tricky
283 $totalNoTax = $order_item->get_total() ?: "0";
284 $tax = $order_item->get_total_tax() ?: "0";
285 $total = $tax+$totalNoTax;
286 $subtotalNoTax = $order_item->get_subtotal() ?: "0";
287 $subtotalTax = $order_item->get_subtotal_tax() ?: "0";
288 $subtotal = $subtotalNoTax + $subtotalTax;
289 $quantity = $order_item->get_quantity();
290 $unitprice = $subtotal/$quantity;
291 // Must do this to avoid rounding errors, since we get floats instead of money here :(
292 $discount = round(100*$subtotal) - round(100*$total);
293 if ($discount < 0) $discount = 0;
294 $product = wc_get_product($prodid);
295 $url = home_url("/");
296 if ($product) {
297 $url = get_permalink($prodid);
298 }
299 $taxpercentageraw = 0;
300 if ($subtotalNoTax > 0) {
301 $taxpercentageraw = (($subtotal - $subtotalNoTax) / $subtotalNoTax)*100;
302 }
303 $taxrate = abs(round(100*$taxpercentageraw));
304 $taxpercentage = abs(round($taxpercentageraw));
305 $unitInfo = [];
306 $orderline['name'] = strip_tags($order_item->get_name());
307 $orderline['id'] = strval($prodid);
308 $orderline['totalAmount'] = round($total*100);
309 $orderline['totalAmountExcludingTax'] = round($totalNoTax*100);
310 $orderline['totalTaxAmount'] = round($tax*100);
311
312 $orderline['taxRate'] = $taxrate;
313 $unitinfo['unitPrice'] = round($unitprice*100);
314 $unitinfo['quantity'] = strval($quantity);
315 $unitinfo['quantityUnit'] = 'PCS';
316 $orderline['unitInfo'] = $unitinfo;
317 $orderline['discount'] = $discount;
318 $orderline['productUrl'] = $url;
319 $orderline['isShipping'] = false;
320 $orderlines[] = $orderline;
321 }
322
323 foreach($order->get_items('fee') as $key=>$order_item) {
324 $orderline = [];
325 $totalNoTax = $order_item->get_total() ?: "0";
326 $tax = $order_item->get_total_tax() ?: "0";
327 $total = $tax+$totalNoTax;
328 $quantity = 1;
329 $taxpercentageraw = 0;
330 if ($totalNoTax > 0) {
331 $taxpercentageraw = (($total - $totalNoTax) / $totalNoTax)*100;
332 }
333 $taxrate = abs(round(100*$taxpercentageraw));
334 $taxpercentage = abs(round($taxpercentageraw));
335 $unitInfo = [];
336 $orderline['name'] = strip_tags($order_item->get_name());
337 $orderline['id'] = substr(sanitize_title($orderline['name']), 0, 254);
338 $orderline['totalAmount'] = round($total*100);
339 $orderline['totalAmountExcludingTax'] = round($totalNoTax*100);
340 $orderline['totalTaxAmount'] = round($tax*100);
341 $orderline['discount'] = 0;
342
343 $orderline['taxRate'] = $taxrate;
344 $orderlines[] = $orderline;
345 }
346
347
348 // Handle shipping
349 foreach( $order->get_items( 'shipping' ) as $item_id => $order_item ){
350 $shippingline = [];
351 $orderline['name'] = strip_tags($order_item->get_name());
352 $orderline['id'] = strval($order_item->get_method_id());
353 if (method_exists($order_item, 'get_instance_id')) {
354 $orderline['id'] .= ":" . strval($order_item->get_instance_id());
355 }
356
357 $totalNoTax = $order_item->get_total() ?: "0";
358 $tax = $order_item->get_total_tax() ?: "0";
359 $total = $tax+$totalNoTax;
360 $subtotalNoTax =$totalNoTax;
361 $subtotalTax = $tax;
362 $subtotal = $subtotalNoTax + $subtotalTax;
363
364 $taxpercentageraw = 0;
365 if ($subtotalNoTax > 0) {
366 $taxpercentageraw = (($subtotal - $subtotalNoTax) / $subtotalNoTax)*100;
367 }
368 $taxpercentage = abs(round($taxpercentageraw));
369 $taxrate= abs(round($taxpercentageraw * 100));
370
371 $orderline['totalAmount'] = round($total*100);
372 $orderline['totalAmountExcludingTax'] = round($totalNoTax*100);
373 $orderline['totalTaxAmount'] = round($tax*100);
374 $orderline['taxRate'] = $taxrate;
375
376 $unitinfo = [];
377
378 $unitinfo['unitPrice'] = round($total*100);
379 $unitinfo['quantity'] = strval(1);
380 $unitinfo['quantityUnit'] = 'PCS';
381 $orderline['unitInfo'] = $unitinfo;
382 $discount = 0;
383 $orderline['discount'] = $discount;
384 $orderline['isShipping'] = true;
385 $orderlines[] = $orderline;
386 }
387
388 $receiptdata['orderLines'] = $orderlines;
389 $receiptdata['bottomLine'] = $bottomline;
390
391 } catch (Exception $e) {
392 $this->log(sprintf(__('Cannot create receipt for order %1$d: %2$s', 'woo-vipps'), $order->get_id(), $e->getMessage()), 'error');
393 }
394 return $receiptdata;
395 }
396
397 public function add_receipt ($order) {
398 if ($order->get_meta('_vipps_receipt_sent')) {
399 return true;
400 }
401 $vippsid = $order->get_meta('_vipps_orderid');
402 if (!$vippsid) {
403 $this->log(sprintf(__("Cannot add receipt for order %1\$d: No vipps id present", 'woo-vipps'), $order->get_id()), 'error');
404 return false;
405 }
406 // Currently ecom or recurring - we are only doing ecom for now IOK 2022-06-20
407 // please note that 'ecom' applies to both ecom and epayment. IOK 2023-12-13
408 $paymenttype = apply_filters('woo_vipps_receipt_type', 'ecom', $order);
409 $command = 'order-management/v2/' . $paymenttype . '/receipts/' . $vippsid;
410 $msn = $this->get_merchant_serial();
411 $headers = $this->get_headers($msn);
412
413 $receiptdata = $this->get_receipt_data($order);
414 if (empty($receiptdata)) {
415 $this->log(__("Could not send receipt to Vipps: ", 'woo-vipps') . $order->getId(), 'error');
416 return false;
417 }
418 try {
419 $res = $this->http_call($msn,$command,$receiptdata,'POST',$headers,'json');
420 $order->update_meta_data('_vipps_receipt_sent', true);
421 $order->save();
422 $this->log(sprintf(__("Receipt for order %1\$d sent to Vipps ", 'woo-vipps'), $order->get_id()), 'info');
423 return true;
424 } catch (Exception $e) {
425 $this->log(__("Could not send receipt to Vipps: ", 'woo-vipps') . $e->getMessage(), 'error');
426 return false;
427 }
428
429
430 }
431
432 // Note that paymenttype 'ecom' applies to both ecom and epayment. IOK 2023-12-13
433 public function add_category($order, $link, $imageid, $categorytype="GENERAL", $paymenttype="ecom") {
434 $vippsid = $order->get_meta('_vipps_orderid');
435 if (!$vippsid) {
436 $this->log(sprintf(__("Cannot add category for order %1\$d: No vipps id present", 'woo-vipps'), $order->get_id()), 'error');
437 return false;
438 }
439
440 $date = gmdate('c');
441 $ip = $_SERVER['SERVER_ADDR'] ?? $_SERVER['LOCAL_ADDR'] ?? '127.0.0.1' ;
442 $at = $this->get_access_token();
443 $subkey = $this->get_key();
444 $msn = $this->get_merchant_serial();
445 $headers = $this->get_headers($msn);
446
447 // Currently ecom or recurring - we are only doing ecom for now IOK 2022-06-20
448 // Note that 'ecom' applies to both ecom and epayment. IOK 2023-12-13
449 $paymenttype = apply_filters('woo_vipps_receipt_type', 'ecom', $order);
450 $command = "order-management/v2/$paymenttype/categories/$vippsid";
451
452 $args = ['category'=>$categorytype, 'orderDetailsUrl' => $link ];
453 if ($imageid) {
454 $args['imageId'] = $imageid;
455 }
456 try {
457 $res = $this->http_call($msn,$command,$args,'PUT',$headers,'json');
458 return true;
459 } catch (Exception $e) {
460 $this->log(sprintf(__("Could not add category %1\$s to Vipps: ", 'woo-vipps'), $categorytype) . $e->getMessage(), 'error');
461 return false;
462 }
463 }
464
465 // Note that paymenttype 'ecom' applies to both ecom and epayment. IOK 2023-12-13
466 public function get_receipt($order, $paymenttype = "ecom") {
467 $vippsid = $order->get_meta('_vipps_orderid');
468 if (!$vippsid) {
469 $this->log(sprintf(__("Cannot add category for order %1\$d: No vipps id present", 'woo-vipps'), $order->get_id()), 'error');
470 return false;
471 }
472 $msn = $this->get_merchant_serial();
473 $headers = $this->get_headers($msn);
474
475 // Currently ecom or recurring - we are only doing ecom for now IOK 2022-06-20
476 // Note that 'ecom' applies to both ecom and epayment. IOK 2023-12-13
477 $paymenttype = apply_filters('woo_vipps_receipt_type', 'ecom', $order);
478 $command = "order-management/v2/$paymenttype/$vippsid";
479 try {
480 $res = $this->http_call($msn,$command,[],'GET',$headers);
481 return $res;
482 } catch (Exception $e) {
483 $this->log(sprintf(__("Could not get receipt data for order %1\$s from Vipps: ", 'woo-vipps'), $order->get_id()) . $e->getMessage(), 'error');
484 return false;
485 }
486
487 }
488 # End Order Management API functions
489
490 // Initiate payment via the epayment API; Express Checkout will still use Ecomm/v2 IOK 2023-12-13
491 // Not any more: Express Checkout is started if the logistics parameters are set. IOK 2025-06-19
492 public function epayment_initiate_payment($phone,$order,$returnurl,$authtoken,$idempotency_key=null) {
493 $command = 'epayment/v1/payments';
494 $msn = $this->get_merchant_serial();
495 $subkey = $this->get_key($msn);
496 $prefix = $this->get_orderprefix();
497 $static_shipping = $order->get_meta('_vipps_static_shipping');
498 $needs_shipping = $order->get_meta('_vipps_needs_shipping');
499
500 if (!$subkey) {
501 throw new VippsAPIConfigurationException(__('The Vipps gateway is not correctly configured.','woo-vipps'));
502 $this->log(__('The Vipps gateway is not correctly configured.','woo-vipps'),'error');
503 }
504 if (!$msn) {
505 throw new VippsAPIConfigurationException(__('The Vipps gateway is not correctly configured.','woo-vipps'));
506 $this->log(__('The Vipps gateway is not correctly configured.','woo-vipps'),'error');
507 }
508
509 if (!$idempotency_key) $idempotency_key = $order->get_order_key();
510
511 // We now support two gateways: 'vipps' and 'vipps_card' for credit card payments.
512 $gateway = $order->get_payment_method();
513
514 // Make sure to clean up old metas, since we now support restarting order payment with Vipps retry sesssions,
515 // so these may be set and cause conflicts. LP 2026-03-11
516 if ($order->get_meta('_vipps_shipping_set')) {
517 $order->delete_meta_data('_vipps_checkout_session');
518 $order->delete_meta_data('_vipps_express_checkout'); // note: This is also set for Checkout. LP 2026-03-12
519 $order->delete_meta_data('_vipps_init_timestamp');
520 $order->delete_meta_data('_vipps_callback_timestamp');
521 $order->delete_meta_data('_vipps_capture_timestamp');
522 $order->delete_meta_data('_vipps_refund_timestamp');
523 $order->delete_meta_data('_vipps_cancel_timestamp');
524 }
525
526 $headers = $this->get_headers($msn);
527
528 $express = $order->get_meta('_vipps_express_checkout');
529
530 // Make it easier to find express checkout orders in Vipps' backend IOK 2025-10-29
531 if ($express) {
532 $headers['Vipps-System-Plugin-Name'] = 'woo-vipps-express';
533 }
534 // And credit card payments the same IOK 2026-05-27
535 if ($gateway == 'vipps_card') {
536 $headers['Vipps-System-Plugin-Name'] = 'woo-vipps-card';
537 }
538
539 // We will use this to retrieve the orders in the callback, since the prefix can change in the admin interface. IOK 2018-05-03
540 // This is really for the new epayment api only, but we do this to ensure we use the same logic. For short prefixes and order numbers.
541 // Pad orderid with 0 to the left so the entire vipps-orderid/reference is at least 8 chars long. IOK 2022-04-06
542 $orderid = $order->get_id();
543 $woovippsid = $prefix . $orderid;
544 $len = strlen($woovippsid);
545 if ($len < 8) { # max is 50 so that would probably not be an issue
546 $padwith = 8 - strlen($prefix);
547 $paddedid = str_pad("".$orderid, $padwith, "0", STR_PAD_LEFT);
548 $woovippsid = $prefix . $paddedid;
549 }
550 $vippsorderid = apply_filters('woo_vipps_orderid', $woovippsid, $prefix, $order);
551
552 // Retry sessions: add retry index to reference and idempotency key. Note: reusing idempotency key even
553 // with a different reference will result in fail. We also dont want to regenerate the order key (which is used as idempotency key by default)
554 // since that may have unwanted effects (e.g. links in session/emails may stop working). LP 2026-03-13
555 // https://developer.vippsmobilepay.com/docs/knowledge-base/orderid/#handling-multiple-payment-attempts-for-the-same-order
556 $retrycount = intval($order->get_meta('_vipps_retry_count'));
557 if ($retrycount) {
558 $vippsorderid .= "-$retrycount";
559 $idempotency_key .= "-$retrycount";
560 }
561 $headers['Idempotency-Key'] = $idempotency_key;
562
563 $order->update_meta_data('_vipps_api', 'epayment');
564 $order->update_meta_data('_vipps_prefix',$prefix);
565 $order->update_meta_data('_vipps_orderid', $vippsorderid);
566 $order->set_transaction_id($vippsorderid); // The Vipps order id is probably the clossest we are getting to a transaction ID IOK 2019-03-04
567 $order->delete_meta_data('_vipps_static_shipping'); // Don't need this any more
568 $order->save();
569
570 $callback = $this->gateway->payment_callback_url($authtoken, $orderid);
571 $fallback = $returnurl;
572
573
574 $data = [];
575 $data['reference'] = $vippsorderid;
576
577 // WEB_REDIRECT is the normal flow; requires a returnUrl. PUSH_MESSAGE requires a valid customer (phone number)
578 // NATIVE_REDIRECT is for automatic app switch between a native app and the Vipps MobilePay app.
579 // QR returns a QR code that can be scanned to complete the payment. IOK 2023-12-13
580 $data['userFlow'] = apply_filters('woo_vipps_payment_user_flow', 'WEB_REDIRECT', $orderid);
581
582 $data['paymentMethod'] = ['type' => 'WALLET']; // This is the Vipps MobilePay app. CARD is credit card, must then use userFlow WEB_REDIRECT
583
584 if ($gateway == 'vipps_card') {
585 $data['paymentMethod'] = ['type' => 'CARD'];
586 $data['userFlow'] = 'WEB_REDIRECT';
587 }
588
589 $data['amount'] = ['currency' => $order->get_currency(), 'value' => round(wc_format_decimal($order->get_total(),'') * 100)];
590 $data['returnUrl'] = $fallback;
591
592 $data['customer'] = [];
593
594 // Allow filters to use CUSTOMER_PRESENT if using in store situation with the user physically present IOK 2023-12-13
595 $data['customer']['customerInteraction'] = apply_filters('woo_vipps_customerInteraction', 'CUSTOMER_NOT_PRESENT', $orderid);
596 if ($phone) {
597 $phonenr = Vipps::normalizePhoneNumber($phone, $order->get_billing_country());
598 if ($phonenr) {
599 $data['customer']['phoneNumber'] = $phonenr;
600 }
601 $data['customer'] = apply_filters('woo_vipps_payment_customer_data',$data['customer'],$orderid);
602 }
603
604 // Store the original orderid as metadata, so we can retrieve it if neccessary IOK 2023-12-21
605 $metadata = [];
606 $metadata['orderid'] = $orderid;
607 $metadata = apply_filters('woo_vipps_payment_metadata', $metadata, $orderid);
608 $data['metadata'] = $metadata;
609
610 // We will not normally ask for user info, but if this is an express checkout order, we do want name, email, phone and maybe address.
611 // A filter will allow advanced users to add scope for their own usage.
612 // When scope has been added, it is possible to get a 'sub' value from the payment details, which for several weeks
613 // can be used to retrieve user information from the user info API. IOK 2023-03-10
614 // IOK 2023-03-10 'scope' determines for what data we ask the customer.
615 // possible values, name, address, email, phoneNumber, birthDate, nin and accountNumbers (last ones are of course restricted)
616 // we need name, email and maybe address for the new express. LP 2025-05-26
617 $scope = array();
618 if ($express) {
619 // The old "explicit shipping" option which is now the only option - if set to "yes", always ask for address
620 $explicit_option = ($this->gateway->get_option('useExplicitCheckoutFlow') == "yes");
621 // Merchant may always need the address, so if so chosen, ask for it
622 $always_address = ($this->gateway->get_option('expresscheckout_always_address') == "yes");
623 $ask_for_address = apply_filters('woo_vipps_express_checkout_ask_for_address', ($needs_shipping || $always_address || $explicit_option), $order);
624
625 // Otherwise we are going for name, email, phone.
626 if ($ask_for_address) {
627 $scope = ["name", "email", "phoneNumber", "address"];
628 } else {
629 $scope = ["name", "email", "phoneNumber"];
630 }
631 $scope = apply_filters('woo_vipps_express_checkout_scope', $scope, $order);
632 }
633
634 $scope = apply_filters('woo_vipps_payment_scope', $scope, $orderid);
635 if (!empty($scope)) {
636 $data['profile'] = [];
637 $data['profile']['scope'] = join(" ", $scope);
638 }
639
640 // minimumUserAge: Integer [0..100] or null. LP 2025-05-26
641 $minage = apply_filters('woo_vipps_payment_minimum_user_age', null);
642 $minageint = intval($minage);
643 // intval defaults to 0 when it fails to convert to int, so make sure this case will default to null, by is_numeric. LP 2025-05-26
644 if (is_numeric($minage) && $minageint >= 0 && $minageint <= 100) {
645 $minage = $minageint;
646 } else {
647 $minage = null;
648 }
649 $data['minimumUserAge'] = $minage;
650
651
652 // Some control over the QR
653 if ($data['userFlow'] == 'QR') {
654 // Formats are IMAGE/SVG+XML, TEXT/TARGETURL, IMAGE/PNG
655 $data['qrFormat'] = ['format' => apply_filters('woo_vipps_payment_qr_format', 'IMAGE/SVG+XML', $orderid),
656 'size' => apply_filters('woo_vipps_payment_qr_size', 1024, $orderid)];
657 }
658
659
660 $shop_identification = apply_filters('woo_vipps_transaction_text_shop_id', home_url());
661 $transactionText = __('Confirm your order from','woo-vipps') . ' ' . $shop_identification;
662 $data['paymentDescription'] = apply_filters('woo_vipps_transaction_text', $transactionText, $order);
663 // The limit for the transaction text is 100. Ensure we don't go over. Thanks to Marco1970 on wp.org for reporting this. IOK 2019-10-17
664 $length = strlen($data['paymentDescription']);
665 if ($length>99) {
666 $this->log(__('The transaction text is too long! We are using a shorter transaction text to allow the transaction text to go through, but please check the \'woo_vipps_transaction_text_shop_id\' filter so that you can use a shorter name for your store', 'woo-vipps'));
667 $data['paymentDescription'] = substr($data['paymentDescription'],0,90); // Add some slack if this happens. IOK 2019-10-17
668 }
669
670 // Epayment can send the receipt already in the initiate call, so lets do it. IOK 2023-12-23
671 $receiptdata = $this->get_receipt_data($order);
672 if (!empty($receiptdata)) {
673 $data['receipt'] = $receiptdata;
674 $order->update_meta_data('_vipps_receipt_sent', true);
675 $order->save();
676 }
677
678 // The 'shipping' setting is to be set if and only if we are using Express Checkout *and* the order does in fact
679 // need shipping. IOK 2025-06-09
680 if ($express && $needs_shipping) {
681 if ($static_shipping) {
682 $data['shipping']['fixedOptions'] = $static_shipping;
683 } else { // dynamic shipping options for express. LP 2025-05-26
684 $shippingcallback = $this->gateway->shipping_details_callback_url($authtoken, $orderid);
685 $shippingoptions = ['callbackUrl' => $shippingcallback, 'callbackAuthorizationToken' => $authtoken];
686 $data['shipping']['dynamicOptions'] = $shippingoptions;
687 }
688 }
689
690 if ($data['receipt'] ?? false) {
691 // Please note: If expiresAt is added, a receipt must also be added.
692 // expiresAt -- control expiry of payment., must be more than 10 minutes, less than 28 days.
693 // format is RFC 3339 so yyyy-mm-ddTH:i:sZ gmt.
694 // These are for payments that can wait for fullfillment for quite a while, not very well suited for normal Woo stores where stock is
695 // an issue. IOK 2023-12-13
696 $expiresAt = apply_filters('woo_vipps_payment_expires_at', false, $orderid);
697 if ($expiresAt !== false) {
698 if (is_string($expiresAt)) {
699 $expiresAt = gmdate('Y-m-d\TH:i:s\Z', strtotime($expiresAt));
700 } elseif (is_int($expiresAt) && $expiresAt > time()) {
701 $expiresAt = gmdate('Y-m-d\TH:i:s\Z', $expiresAt);
702 } else {
703 $expiresAt = false;
704 }
705 }
706 if ($expiresAt) $data['expiresAt'] = $expiresAt;
707 }
708
709 // Arbitrary metadata that will be retrieved in payment responses. Please note, key length is <= 100, value <= 500, max elements is 5
710 $metadata_raw = [];
711 $metadata_filtered = apply_filters('woo_vipps_payment_metadata', $metadata_raw, $orderid);
712 $metadata = [];
713 $i = 0;
714 foreach($metadata_filtered as $key => $value) {
715 if (strlen($key)>100 || strlen($value) > 500) {
716 $this->log(sprintf(__('Could not add key %1$s to payment metadata of order %2$s - key or value is too long (100, 500 respectively)', 'woo-vipps'), $key, $orderid));
717 continue;
718 }
719 $i++;
720 if ($i > 5) {
721 $this->log(sprintf(__('Could not add all keys to the payment metadata of order %1$s - only 5 items are allowed', 'woo-vipps'), $orderid));
722 break;
723 }
724 $metadata[$key] = $value;
725 }
726 if (!empty($metadata)) {
727 $data['metadata'] = $metadata;
728 }
729
730 $this->log("Initiating Vipps MobilePay epayment session for $vippsorderid", 'debug');
731 $data = apply_filters('woo_vipps_epayment_initiate_payment_data', $data);
732
733 // Now for QR, this value will be an URL to the QR code, or the target URL. If the flow is PUSH_MESSAGE, nothing will be returned.
734 $res = $this->http_call($msn,$command,$data,'POST',$headers,'json');
735
736 // Backwards compatibility: Previous API returned this as an URL. We also get a 'reference' back, the Vipps Order Id
737 $res['url'] = $res['redirectUrl'] ?? false;
738 return $res;
739 }
740
741
742 // This is Vipps Checkout IOK 2021-06-19
743 // Updated for V3 2023-01-09
744 public function initiate_checkout($customerinfo,$order,$returnurl,$authtoken,$idempotency_key=null) {
745 $command = 'checkout/v3/session';
746 $static_shipping = $order->get_meta('_vipps_static_shipping');
747 $needs_shipping = $order->get_meta('_vipps_needs_shipping');
748
749 $msn = $this->get_merchant_serial();
750 $subkey = $this->get_key($msn);
751 $clientid = $this->get_clientid($msn);
752 $secret = $this->get_secret($msn);
753 $prefix = $this->get_orderprefix();
754 // Don't go on with the order, but don't tell the customer too much. IOK 2018-04-24
755 if (!$subkey) {
756 throw new VippsAPIConfigurationException(__('The Vipps gateway is not correctly configured.','woo-vipps'));
757 $this->log(__('The Vipps gateway is not correctly configured.','woo-vipps'),'error');
758 }
759 if (!$msn) {
760 throw new VippsAPIConfigurationException(__('The Vipps gateway is not correctly configured.','woo-vipps'));
761 $this->log(__('The Vipps gateway is not correctly configured.','woo-vipps'),'error');
762 }
763
764 if (!$idempotency_key) $idempotency_key = $order->get_order_key();
765
766 // We will use this to retrieve the orders in the callback, since the prefix can change in the admin interface. IOK 2018-05-03
767 // Pad orderid with 0 to the left so the entire vipps-orderid/reference is at least 8 chars long. IOk 2022-04-06
768 $orderid = $order->get_id();
769 $woovippsid = $prefix . $orderid;
770 $len = strlen($woovippsid);
771 if ($len < 8) { # max is 50 so that would probably not be an issue
772 $padwith = 8 - strlen($prefix);
773 $paddedid = str_pad("".$orderid, $padwith, "0", STR_PAD_LEFT);
774 $woovippsid = $prefix . $paddedid;
775 }
776 $vippsorderid = apply_filters('woo_vipps_orderid', $woovippsid, $prefix, $order);
777
778
779 $order->update_meta_data('_vipps_prefix',$prefix);
780 $order->update_meta_data('_vipps_orderid', $vippsorderid);
781 $order->set_transaction_id($vippsorderid); // The Vipps order id is probably the clossest we are getting to a transaction ID IOK 2019-03-04
782 # $order->delete_meta_data('_vipps_static_shipping'); // Don't need this any more
783 $order->save();
784
785 $headers = $this->get_headers($msn);
786 // Required for Checkout
787 $headers['client_id'] = $clientid;
788 $headers['client_secret'] = $secret;
789
790 $headers['Idempotency-Key'] = $idempotency_key;
791
792 // Object to send.
793 $data = array();
794 $data['reference'] = $vippsorderid;
795
796 // The string returned is a prefix ending with callback=, for v3 we need to send a complete URL
797 // so we just add the callback type here.
798 $callback = $this->gateway->payment_callback_url($authtoken,$orderid) . "checkout";
799 $fallback = $returnurl;
800
801 $transaction = array();
802 $currency = $order->get_currency();
803 // Ignore refOrderId - for child-transactions
804 $transaction['reference'] = $vippsorderid;
805 $transaction['amount'] = array('value' => round(wc_format_decimal($order->get_total(),'') * 100), 'currency' => $currency);
806 $shop_identification = apply_filters('woo_vipps_transaction_text_shop_id', home_url());
807 $transactionText = __('Confirm your order from','woo-vipps') . ' ' . $shop_identification;
808 $transaction['paymentDescription'] = apply_filters('woo_vipps_transaction_text', $transactionText, $order);
809
810 // The limit for the transaction text is 100. Ensure we don't go over. Thanks to Marco1970 on wp.org for reporting this. IOK 2019-10-17
811 $length = strlen($transaction['paymentDescription']);
812 if ($length>99) {
813 $this->log(__('The transaction text is too long! We are using a shorter transaction text to allow the transaction text to go through, but please check the \'woo_vipps_transaction_text_shop_id\' filter so that you can use a shorter name for your store', 'woo-vipps'));
814 $transaction['paymentDescription'] = substr($transaction['paymentDescription'],0,90); // Add some slack if this happens. IOK 2019-10-17
815 }
816
817
818 ## Vipps Checkout Shipping
819 $shippingcallback = $this->gateway->shipping_details_callback_url($authtoken, $orderid);
820 $shippingcallback .= "/v3/checkout/" . $vippsorderid . "/shippingDetails"; # because this is how eCom v2 does it.
821 $gw = $this->gateway;
822 if ($needs_shipping) {
823 $logistics = array();
824 if ($static_shipping) {
825 $logistics['fixedOptions'] = $static_shipping["shippingDetails"];
826 unset($logistics['dynamicOptionsCallback']);
827 } else {
828 $logistics['dynamicOptionsCallback'] = $shippingcallback;
829 }
830
831 // Add integration data if present
832 $integrations = array();
833 if ($gw->get_option('vcs_porterbuddy') == 'yes') {
834 $porterbuddy = array();
835 $porterbuddy['publicToken'] = $gw->get_option('vcs_porterbuddy_publicToken');
836 $porterbuddy['apiKey'] = $gw->get_option('vcs_porterbuddy_apiKey');
837 $origin = array();
838 $origin['name'] = get_bloginfo('name');
839 $origin['phoneNumber'] = $gw->get_option('vcs_porterbuddy_phoneNumber');
840 $origin['email'] = get_option('admin_email');
841 $address = array();
842 $address['streetAddress'] = join(", ", [WC()->countries->get_base_address(), WC()->countries->get_base_address_2()]);
843 $address['postalCode'] = WC()->countries->get_base_postcode();
844 $address['city'] = WC()->countries->get_base_city();
845 $address['country'] = WC()->countries->get_base_country();
846 $origin['address'] = $address;
847 $porterbuddy['origin'] = apply_filters('woo_vipps_porterbuddy_origin', $origin);
848 $integrations['porterbuddy'] = $porterbuddy;
849 }
850
851 if ($gw->get_option('vcs_helthjem') == 'yes') {
852 $helthjem = array();
853 $helthjem['username'] = $gw->get_option('vcs_helthjem_username');
854 $helthjem['password'] = $gw->get_option('vcs_helthjem_password');
855 $helthjem['shopId'] = $gw->get_option('vcs_helthjem_shopId');
856 $integrations['helthjem'] = $helthjem;
857 }
858 if (!empty($integrations)) {
859 // 'logistics.integrations' deprecated in Checkout: https://developer.vippsmobilepay.com/api/checkout/#tag/Session/paths/~1checkout~1v3~1session/post. LP 2025-07-10
860 $logistics['integrations'] = $integrations;
861 }
862 $data['logistics'] = $logistics;
863 }
864
865 // IOK 2025-03-26 currenlty only legal value
866 $data['type'] = "PAYMENT";
867
868 if (!empty($customerinfo)) {
869 $data['prefillCustomer'] = $customerinfo;
870 }
871
872 # This have to exist, but we'll not check it now.
873 if (! function_exists("wc_terms_and_conditions_page_id")) {
874 $msg = sprintf(__('You need a newer version of WooCommerce to use %1$s!', 'woo-vipps'), Vipps::CheckoutName());
875 $this->log($msg, 'error');;
876 throw new Exception($msg);
877 }
878 $termsAndConditionsUrl = get_permalink(wc_terms_and_conditions_page_id());
879 $data['merchantInfo'] = array('callbackAuthorizationToken'=>$authtoken, 'callbackUrl'=>$callback, 'returnUrl'=>$fallback);
880 if (!empty($termsAndConditionsUrl)) {
881 $data['merchantInfo']['termsAndConditionsUrl'] = $termsAndConditionsUrl;
882 } else {
883 $this->log(sprintf(__('Your site does not have a Terms and Conditions page defined - starting %1$s anyway, but this should be defined', 'woo-vipps'), Vipps::CheckoutName()));
884 }
885
886 // From v3: Certain data moved to a 'configuration' field
887 $configuration = [];
888 $configuration['elements'] = "Full";
889 $configuration['customerInteraction'] = apply_filters('woo_vipps_checkout_customerInteraction', 'CUSTOMER_NOT_PRESENT', $orderid);
890 $configuration['userFlow'] = "WEB_REDIRECT"; // Change to NATIVE_REDIRECT for apps in below filter
891 // Require consent of email and openid sub - really for login
892 $configuration['requireUserInfo'] = (bool) apply_filters('woo_vipps_checkout_requireUserInfo', $gw->get_option('requireUserInfo_checkout') == 'yes' , $orderid);
893
894
895 // IOK 2023-12-22 and we can add an order summary, so do so by default
896 $summarize = apply_filters('woo_vipps_checkout_show_order_summary', true, $order);
897 if ($summarize) {
898 $ordersummary = $this->get_receipt_data($order);
899 // This is different in the receipt api, the epayment api and in checkout.
900 $ordersummary['orderBottomLine'] = $ordersummary['bottomLine'];
901 unset($ordersummary['bottomLine']);
902
903 // Don't finalize the receipt number - we just want to show this rn.
904 unset($ordersummary['orderBottomLine']['receiptNumber']);
905 if (!empty($ordersummary)) {
906 $transaction['orderSummary'] = $ordersummary;
907 $configuration['showOrderSummary'] = true;
908
909 // Currently, for checkout, *this counts as a receipt*, even though it lacks shipping.
910 // (As of 2025-03-20, not a bug, but it may be one when modifying the order )
911 $order->update_meta_data('_vipps_receipt_sent', true);
912 $order->save();
913
914 }
915 }
916
917 // ISO-3166 Alpha 2 country list
918 $countries = array_keys((new WC_Countries())->get_allowed_countries());
919 $allowed_countries = apply_filters('woo_vipps_checkout_countries', $countries, $orderid);
920 if ($allowed_countries) {
921 $configuration['countries'] = ['supported' => $allowed_countries ];
922 } else {
923
924 }
925
926 // External payment methods IOK 2024-05-13
927 // Should return a map from other_method => ['gw'=>'gateway key or any or empty string]
928 $other_payment_methods = apply_filters('woo_vipps_checkout_external_payment_methods', VippsCheckout::instance()->external_payment_methods(), $order);
929 if (!empty($other_payment_methods)) {
930 $others = [];
931 foreach ($other_payment_methods as $methodkey => $methoddata) {
932 $chooseanother = ['action'=>'vipps_gw', 'o'=>$orderid];
933 $chooseanother['cb'] = wp_create_nonce('vipps_gw');
934 $chooseanother['gw'] = ($methoddata['gw'] ?? "");
935 $others[] = ['paymentMethod' => $methodkey, 'redirectUrl'=> add_query_arg($chooseanother,admin_url("admin-post.php")) ];
936 }
937 if (!empty($others)) {
938 $configuration['externalPaymentMethods'] = $others;
939 }
940 }
941
942 // Custom consent checkbox, for integration with Mailchimp etc .
943 $customconsenttext = apply_filters('woo_vipps_checkout_consent_query', "");
944 $customconsentrequired = apply_filters('woo_vipps_checkout_consent_required', false);
945 if ($customconsenttext) {
946 $customconsent = [];
947 $customconsent['text'] = $customconsenttext;
948 $customconsent['required'] = $customconsentrequired;
949 $configuration['customConsent'] = $customconsent;
950 }
951
952 if (!$needs_shipping) {
953 $nocontacts = (bool) ($this->gateway->get_option('noContactFields') == 'yes');
954 $noaddress = (bool) ($this->gateway->get_option('noAddressFields') == 'yes');
955 if ($noaddress) {
956 $configuration['elements'] = "PaymentAndContactInfo";
957 }
958 // AddressFields cannot be enabled while ContactFields is disabled
959 if ($noaddress && $nocontacts) {
960 $configuration['elements'] = "PaymentOnly";
961 }
962 }
963 $data['configuration'] = $configuration;
964 $data['transaction'] = $transaction;
965
966 $data = apply_filters('woo_vipps_initiate_checkout_data', $data);
967
968 $this->log("Initiating Checkout session for $vippsorderid", 'debug');
969 $res = $this->http_call($msn,$command,$data,'POST',$headers,'json');
970 return $res;
971 }
972
973 // If an order materially changes, we need to call this to change the sum total and order description at Vipps. IOK 2025-04-11
974 public function checkout_modify_session($order, $updated_shipping=null) {
975 $command = 'checkout/v3/session';
976 $msn = $this->get_merchant_serial();
977 $subkey = $this->get_key($msn);
978 $clientid = $this->get_clientid($msn);
979 $secret = $this->get_secret($msn);
980
981 $orderid = $order->get_id();
982 $vippsorderid = $order->get_meta('_vipps_orderid');
983 $reference = $vippsorderid;
984
985 $headers = $this->get_headers($msn);
986 // Required for Checkout
987 $headers['client_id'] = $clientid;
988 $headers['client_secret'] = $secret;
989
990 // Transaction: amount, description, orderSummary for modify.
991 $transaction = array();
992 $currency = $order->get_currency();
993
994 $total = round(wc_format_decimal($order->get_total(),'') * 100);
995 if ($total < 100) $total = 100; // Vipps requires all orders to be at least this large IOK 2025-05-14
996
997 $transaction['amount'] = array('value' => $total, 'currency' => $currency);
998 $shop_identification = apply_filters('woo_vipps_transaction_text_shop_id', home_url());
999 $transactionText = __('Confirm your order from','woo-vipps') . ' ' . $shop_identification;
1000 $transaction['paymentDescription'] = apply_filters('woo_vipps_transaction_text', $transactionText, $order);
1001 $summarize = apply_filters('woo_vipps_checkout_show_order_summary', true, $order);
1002 if ($summarize) {
1003 $ordersummary = $this->get_receipt_data($order);
1004 // This is different in the receipt api, the epayment api and in checkout.
1005 $ordersummary['orderBottomLine'] = $ordersummary['bottomLine'];
1006 unset($ordersummary['bottomLine']);
1007
1008 // Don't finalize the receipt number - we just want to show this rn.
1009 unset($ordersummary['orderBottomLine']['receiptNumber']);
1010 if (!empty($ordersummary)) {
1011 $transaction['orderSummary'] = $ordersummary;
1012 }
1013 }
1014 $data = ['transaction'=>$transaction];
1015 // Probably mostly because free shipping has been added or removed using coupons. IOK 2025-09-12
1016 if ($updated_shipping) {
1017 $data['logisticOptions'] = $updated_shipping;
1018 }
1019 $res = $this->http_call($msn,$command . "/" . urlencode($reference),$data,'PATCH',$headers,'json');
1020 return $res;
1021 }
1022 public function checkout_expire_session($order) {
1023 $msn = $this->get_merchant_serial();
1024 $clientid = $this->get_clientid($msn);
1025 $secret = $this->get_secret($msn);
1026 $vippsorderid = $order->get_meta('_vipps_orderid');
1027 $reference = $vippsorderid;
1028 $command = "checkout/v3/session/$reference/expire";
1029
1030 $headers = $this->get_headers($msn);
1031 // Required for Checkout
1032 $headers['client_id'] = $clientid;
1033 $headers['client_secret'] = $secret;
1034
1035 $res = $this->http_call($msn, $command, [], 'POST', $headers, 'json');
1036 return $res;
1037 }
1038
1039 // Returns same data as session poll; we've changed it so 404s and so returns as words
1040 public function checkout_get_session_info($order) {
1041 $command = 'checkout/v3/session';
1042 $vippsid = $order->get_meta('_vipps_orderid');
1043 $command .= "/" . $vippsid;
1044
1045 $msn = $this->get_merchant_serial();
1046 $headers = $this->get_headers($msn);
1047 $clientid = $this->get_clientid($msn);
1048 $secret = $this->get_secret($msn);
1049
1050 $headers = $this->get_headers($msn);
1051 // Required for checkout
1052 $headers['client_id'] = $clientid;
1053 $headers['client_secret'] = $secret;
1054 $data = [];
1055
1056 $res = "ERROR";
1057 try {
1058 $res = $this->http_call($msn,$command,$data,'GET',$headers,'json');
1059 if (($res['sessionState'] ?? "") == 'SessionExpired') {
1060 return 'EXPIRED';
1061 }
1062 } catch (VippsAPIException $e) {
1063 if ($e->responsecode == 404) {
1064 return 'EXPIRED';
1065 } else {
1066 $this->log(sprintf(__("Error polling status - error message %1\$s", 'woo-vipps'), $e->getMessage()));
1067 // We can't do much more than this so just return ERROR
1068 return 'ERROR';
1069 }
1070 } catch (Exception $e) {
1071 $this->log(sprintf(__("Error polling status - error message %1\$s", 'woo-vipps'), $e->getMessage()));
1072 // We can't dom uch more than this so just return ERROR
1073 return 'ERROR';
1074 }
1075 return $res;
1076 }
1077
1078
1079 // Support for then new epayment API, which is also used by Checkout
1080 // Cancel a reserved but not captured payment IOK 2018-05-07
1081 // Currently must cancel the entire amount, but partial cancel will be possible.
1082 public function epayment_cancel_payment($order,$requestid=1) {
1083 $orderid = $order->get_meta('_vipps_orderid');
1084 $command = 'epayment/v1/payments/'.$orderid.'/cancel';
1085 $msn = $this->get_merchant_serial();
1086 $subkey = $this->get_key($msn);
1087 if (!$subkey) {
1088 throw new VippsAPIConfigurationException(__('The Vipps gateway is not correctly configured.','woo-vipps'));
1089 $this->log(__('The Vipps gateway is not correctly configured.','woo-vipps'),'error');
1090 }
1091 if (!$msn) {
1092 throw new VippsAPIConfigurationException(__('The Vipps gateway is not correctly configured.','woo-vipps'));
1093 $this->log(__('The Vipps gateway is not correctly configured.','woo-vipps'),'error');
1094 }
1095
1096 $headers = $this->get_headers($msn);
1097 $headers['Idempotency-Key'] = $requestid;
1098
1099 // The only current allowed argument is "cancelTransactionOnly" which will, if true, only cancel
1100 // non-authorized transactions. We don't need that, but we have to send *something* or we get type errors. IOK 2024-11-25
1101 $data = array('cancelTransactionOnly' => false);
1102
1103 $res = $this->http_call($msn,$command,$data,'POST',$headers,'json');
1104 return $res;
1105 }
1106
1107 // Support for then new epayment API, which is also used by Checkout
1108 // Capture (a part of) reserved but not captured payment IOK 2018-05-07
1109 public function epayment_capture_payment($order, $amount, $requestid=1) {
1110 $orderid = $order->get_meta('_vipps_orderid');
1111 $command = 'epayment/v1/payments/'.$orderid.'/capture';
1112 $msn = $this->get_merchant_serial();
1113 $subkey = $this->get_key($msn);
1114 if (!$subkey) {
1115 throw new VippsAPIConfigurationException(__('The Vipps gateway is not correctly configured.','woo-vipps'));
1116 $this->log(__('The Vipps gateway is not correctly configured.','woo-vipps'),'error');
1117 }
1118 if (!$msn) {
1119 throw new VippsAPIConfigurationException(__('The Vipps gateway is not correctly configured.','woo-vipps'));
1120 $this->log(__('The Vipps gateway is not correctly configured.','woo-vipps'),'error');
1121 }
1122
1123 $clientid = $this->get_clientid();
1124 $secret = $this->get_secret();
1125 $headers = $this->get_headers($msn);
1126 $headers['Idempotency-Key'] = $requestid;
1127
1128 $modificationAmount = round($amount);
1129 $modificationCurrency = $order->get_currency();
1130
1131 $data = array();
1132 $data['modificationAmount'] = array('value'=>$modificationAmount, 'currency'=>$modificationCurrency);
1133
1134 $res = $this->http_call($msn,$command,$data,'POST',$headers,'json');
1135 return $res;
1136 }
1137
1138 // Support for then new epayment API, which is also used by Checkout
1139 // Refund (a part of) captured payment IOK 2018-05-07
1140 public function epayment_refund_payment($order, $requestid, $amount, $cents) {
1141 $orderid = $order->get_meta('_vipps_orderid');
1142 $command = 'epayment/v1/payments/'.$orderid.'/refund';
1143
1144 # null amount means the entire thing
1145 $amount = $amount ? $amount : wc_format_decimal($order->get_total(),'');
1146
1147 $msn = $this->get_merchant_serial();
1148 $subkey = $this->get_key($msn);
1149 if (!$subkey) {
1150 throw new VippsAPIConfigurationException(__('The Vipps gateway is not correctly configured.','woo-vipps'));
1151 $this->log(__('The Vipps gateway is not correctly configured.','woo-vipps'),'error');
1152 }
1153 if (!$msn) {
1154 throw new VippsAPIConfigurationException(__('The Vipps gateway is not correctly configured.','woo-vipps'));
1155 $this->log(__('The Vipps gateway is not correctly configured.','woo-vipps'),'error');
1156 }
1157
1158 $headers = $this->get_headers($msn);
1159 $headers['Idempotency-Key'] = $requestid;
1160
1161 // If we have passed the value as 'øre' we don't need to calculate any more, but woo is weird so we might need to
1162 $modificationAmount = round($amount);
1163 if ($cents) {
1164 $modificationAmount = round($amount);
1165 } else {
1166 $modificationAmount = round($amount * 100);
1167 }
1168 $modificationCurrency = $order->get_currency();
1169
1170 $data = array();
1171 $data['modificationAmount'] = array('value'=>$modificationAmount, 'currency'=>$modificationCurrency);
1172
1173 $res = $this->http_call($msn,$command,$data,'POST',$headers,'json');
1174 return $res;
1175 }
1176
1177 // For the new epayment API, also used by checkout, return payment details (but not the payment log). Equivalent to the old get-status + metainfo.
1178 // Takes either an order object or the Vipps orderid as argument.
1179 public function epayment_get_payment ($order, $msn='') {
1180 if (is_a($order, 'WC_Order')) {
1181 $orderid = $order->get_meta('_vipps_orderid');
1182 } else {
1183 $orderid = $order;
1184 }
1185 $command = 'epayment/v1/payments/'.$orderid;
1186
1187 if (!$msn) {
1188 $msn = $this->get_merchant_serial();
1189 }
1190
1191 $subkey = $this->get_key($msn);
1192 if (!$subkey) {
1193 throw new VippsAPIConfigurationException(__('The Vipps gateway is not correctly configured.','woo-vipps'));
1194 $this->log(__('The Vipps gateway is not correctly configured.','woo-vipps'),'error');
1195 }
1196 if (!$msn) {
1197 throw new VippsAPIConfigurationException(__('The Vipps gateway is not correctly configured.','woo-vipps'));
1198 $this->log(__('The Vipps gateway is not correctly configured.','woo-vipps'),'error');
1199 }
1200 $headers = $this->get_headers($msn);
1201
1202 $data = array();
1203
1204 $res = $this->http_call($msn,$command,$data,'GET',$headers);
1205 return $res;
1206 }
1207
1208 // For the new epayment API, also used by checkout, return payment log (as for old payment_details. Will be used for debugging.
1209 // epayment api.
1210 public function epayment_get_payment_log ($order) {
1211 $orderid = $order->get_meta('_vipps_orderid');
1212 $command = 'epayment/v1/payments/'.$orderid . "/events";
1213
1214 $msn = $this->get_merchant_serial();
1215 $subkey = $this->get_key($msn);
1216 if (!$subkey) {
1217 throw new VippsAPIConfigurationException(__('The Vipps gateway is not correctly configured.','woo-vipps'));
1218 $this->log(__('The Vipps gateway is not correctly configured.','woo-vipps'),'error');
1219 }
1220 if (!$msn) {
1221 throw new VippsAPIConfigurationException(__('The Vipps gateway is not correctly configured.','woo-vipps'));
1222 $this->log(__('The Vipps gateway is not correctly configured.','woo-vipps'),'error');
1223 }
1224 $headers = $this->get_headers($msn);
1225
1226 $data = array();
1227
1228 $res = $this->http_call($msn,$command,$data,'GET',$headers);
1229 return $res;
1230 }
1231
1232 // Implement part of the userInfo api, just to be able to get user data from Express Orders that aren't express
1233 // orders (because they didn't need shipping).
1234 // If a user has a sub, it is because we've added a scope to the epayment call, or because of integration with login.
1235 // This is not used as of 2026-08-18, because we do not need to call this to retreive user details any more with ecom -
1236 // we get that by just calling the payment details. Still may be useful in the future without depending on login integration. IOK 2026-08-18
1237 public function get_userinfo($sub) {
1238 $command = "vipps-userinfo-api/userinfo" . "/" . $sub;
1239 $msn = $this->get_merchant_serial();
1240 $subkey = $this->get_key($msn);
1241 if (!$subkey) {
1242 throw new VippsAPIConfigurationException(__('The Vipps gateway is not correctly configured.','woo-vipps'));
1243 $this->log(__('The Vipps gateway is not correctly configured.','woo-vipps'),'error');
1244 }
1245 if (!$msn) {
1246 throw new VippsAPIConfigurationException(__('The Vipps gateway is not correctly configured.','woo-vipps'));
1247 $this->log(__('The Vipps gateway is not correctly configured.','woo-vipps'),'error');
1248 }
1249 $headers = $this->get_headers($msn);
1250
1251 $data = array();
1252
1253 $res = $this->http_call($msn,$command,$data,'GET',$headers);
1254 return $res;
1255 }
1256
1257
1258 // the QR api 2022-04-13. PUT is update (on id), DELETE is deletion (of id), GET is get the .. thing. Arguments would be Accept for image/png or image/svg+xml
1259 public function get_merchant_redirect_qr_entry ($id,$accept="text/targetUrl") {
1260 return $this->call_qr_merchant_redirect("GET", $id, null, $accept);
1261 }
1262 public function get_all_merchant_redirect_qr () {
1263 return $this->call_qr_merchant_redirect("GET", "", null, "text/targetUrl");
1264 }
1265 public function create_merchant_redirect_qr ($id,$url){
1266 $action = "POST";
1267 return $this->call_qr_merchant_redirect($action, $id, $url);
1268 }
1269 public function update_merchant_redirect_qr ($id, $url) {
1270 $action = "PUT";
1271 return $this->call_qr_merchant_redirect($action, $id, $url);
1272 }
1273 public function delete_merchant_redirect_qr ($id) {
1274 $action = "DELETE";
1275 return $this->call_qr_merchant_redirect($action, $id, $url);
1276 }
1277 private function call_qr_merchant_redirect($action, $id, $url=null, $accept='image/svg+xml') {
1278 $command = 'qr/v1/merchant-redirect/';
1279 if ($action != "POST") $command .= $id;
1280 $msn = $this->get_merchant_serial();
1281 $subkey = $this->get_key($msn);
1282 if (!$subkey) {
1283 throw new VippsAPIConfigurationException(__('The Vipps gateway is not correctly configured.','woo-vipps'));
1284 $this->log(__('The Vipps gateway is not correctly configured.','woo-vipps'),'error');
1285 }
1286 if (!$msn) {
1287 throw new VippsAPIConfigurationException(__('The Vipps gateway is not correctly configured.','woo-vipps'));
1288 $this->log(__('The Vipps gateway is not correctly configured.','woo-vipps'),'error');
1289 }
1290 $headers = $this->get_headers($msn);
1291 $headers['Accept'] = $accept;
1292
1293 $data = array();
1294 if ($id) $data['id'] = $id;
1295 if ($url) $data['redirectUrl'] = $url;
1296
1297 $res = $this->http_call($msn,$command,$data,$action,$headers, 'json');
1298
1299 return $res;
1300 }
1301
1302 // This isn't really neccessary since we can do this using just the fetch apis, but we'll do it anyway.
1303 // The URLs here are valid for just one hour, so this should be called right after an update.
1304 public function get_merchant_redirect_qr ($url, $accept = "image/svg+xml") {
1305 $msn= $this->get_merchant_serial();
1306 $subkey = $this->get_key($msn);
1307 if (!$subkey) {
1308 throw new VippsAPIConfigurationException(__('The Vipps gateway is not correctly configured.','woo-vipps'));
1309 $this->log(__('The Vipps gateway is not correctly configured.','woo-vipps'),'error');
1310 }
1311 if (!$msn) {
1312 throw new VippsAPIConfigurationException(__('The Vipps gateway is not correctly configured.','woo-vipps'));
1313 $this->log(__('The Vipps gateway is not correctly configured.','woo-vipps'),'error');
1314 }
1315 $headers = $this->get_headers($msn);
1316 $headers['Accept'] = $accept;
1317
1318 $res = $this->http_call($msn, $url,[],'GET',$headers);
1319 return $res;
1320 }
1321
1322 // Conveniently call Vipps IOK 2018-04-18
1323 private function http_call($msn,$command,$data,$verb='GET',$headers=null,$encoding='url'){
1324 $url = "";
1325 if (preg_match("/^http/i", $command)) {
1326 $url = $command;
1327 } else {
1328 $server=$this->gateway->apiurl($msn);
1329 $url = $server . "/" . $command;
1330 }
1331
1332 if (!$headers) $headers=array();
1333 $date = gmdate('c');
1334 $data_encoded = '';
1335 if ($encoding == 'url' || $verb == 'GET') {
1336 $data_encoded = http_build_query($data);
1337 } else {
1338 $data_encoded = json_encode($data, JSON_THROW_ON_ERROR);
1339 }
1340 $data_len = strlen ($data_encoded);
1341 $http_response_header = null;
1342
1343 $headers['Connection'] = 'close';
1344 if ($verb=='POST' || $verb == 'PATCH' || $verb == 'PUT') {
1345 $headers['Content-length'] = $data_len;
1346 if ($encoding == 'url') {
1347 $headers['Content-type'] = 'application/x-www-form-urlencoded';
1348 } else {
1349 $headers['Content-type'] = 'application/json';
1350 }
1351 }
1352 $args = array();
1353 $args['method'] = $verb;
1354 $args['headers'] = $headers;
1355 if ($verb == 'POST' || $verb == 'PATCH' || $verb == 'PUT') {
1356 $args['body'] = $data_encoded;
1357 }
1358 if ($verb == 'GET' && $data_encoded) {
1359 $url .= "?$data_encoded";
1360 }
1361
1362 $return = wp_remote_request($url,$args);
1363 $headers = array();
1364 $content=NULL;
1365 $response=0;
1366
1367 if (is_wp_error($return)) {
1368 $headers['status'] = "500 " . $return->get_error_message();
1369 $response = 500;
1370 } else {
1371 $response = wp_remote_retrieve_response_code($return);
1372 $message = wp_remote_retrieve_response_message($return);
1373
1374
1375 $headers = wp_remote_retrieve_headers($return);
1376 $headers['status'] = "$response $message";
1377 $contenttext = wp_remote_retrieve_body($return);
1378
1379 if ($contenttext) {
1380 $content = @json_decode($contenttext,true);
1381 // Assume we always get json, except for when we don't. IOK 2022-04-22.
1382 if (!$content && !empty($contenttext) && !preg_match("!json!i", $headers['content-type'])){
1383 $content = array('message' => $contenttext);
1384 }
1385 }
1386 }
1387
1388 // Parse the result, converting it to exceptions if neccessary. IOK 2018-05-11
1389 return $this->handle_http_response($msn, $response,$headers,$content);
1390 }
1391
1392 // Read the response from Vipps - if any - and convert errors (null results, results over 299)
1393 // to Exceptions IOK 2018-05-11
1394 private function handle_http_response ($msn, $response, $headers, $content) {
1395 // This would be an error in the URL or something - or a network outage IOK 2018-04-24
1396 // we will assume it is temporary (ie, no response).
1397 if (!$response) {
1398 $msg = __('No response from Vipps', 'woo-vipps');
1399 throw new TemporaryVippsAPIException($msg);
1400 }
1401
1402 // Good result!
1403 if ($response < 300) {
1404 return $content;
1405 }
1406
1407 // Now errorhandling. Default to use just the error header IOK 2018-05-11
1408 $msg = "MSN $msn " . $headers['status'] . " ";
1409
1410 // Sometimes we get one type of error, sometimes another, depending on which layer explodes. IOK 2018-04-24
1411 if ($content) {
1412 // can't happen, but be sure
1413 if (is_string($content)) {
1414 $msg .= " " . $content;
1415 // From initiate payment, at least some times. IOK 2018-06-18
1416 } elseif (isset($content['message'])) {
1417 $msg .= " " . $content['message'];
1418 // From the receipt api
1419 } elseif (isset($content['detail'])) {
1420 $msg .= (isset($content['title'])) ? (" " . $content['title']) : "";
1421 $msg .= ": " . $content['detail'];
1422 if (isset($content['extraDetails'])) {
1423 $msg .= "Extra details: " . print_r($content['extraDetails'], true);
1424 }
1425 } elseif (isset($content['errors'])) {
1426 $msg .= print_r($content['errors'], true);
1427 } elseif (isset($content['error'])) {
1428 // This seems to be only for the Access Token, which is a separate application IOK 2018-05-11
1429 $msg .= $content['error'];
1430 } elseif (isset($content['ResponseInfo'])) {
1431 // This seems to be an error in the API layer. The error is in this elements' ResponseMessage
1432 $msg .= $response . ' ' . $content['ResponseInfo']['ResponseMessage'];
1433 } elseif (isset($content['errorInfo'])) {
1434 $msg .= $response . ' ' . $content['errorInfo']['errorMessage'];
1435 } elseif (isset($content['type'])) {
1436 // The epayment API, version 1
1437 $msg .= $content['title'];
1438 if (isset($content['detail'])) $msg .= " - " . $content['detail'];
1439 if (isset($content['extraDetails'])) $msg .= " - " . print_r($content['extraDetails'], true);
1440 } else {
1441 // Otherwise, we get a simple array of objects with error messages. Grab them all.
1442 $msg .= '';
1443 if (is_array($content)) {
1444 foreach($content as $entry) {
1445 if (is_string($entry)) {
1446 // This started happening august 2023.
1447 $msg .= $entry . "\n";
1448 } elseif (is_array($entry)) {
1449 $msg .= $response . ' ' . @$entry['errorMessage'] . "\n";
1450 } else {
1451 $msg = $response . " " . print_r($content, true);
1452 }
1453 }
1454 } else {
1455 // At this point, we have no idea what we have got, so just stringify it IOK 2021-11-04
1456 $msg .= print_r($msg, true);
1457 }
1458 }
1459 }
1460
1461 // 502's are Bad Gateway which means that Vipps is busy. IOK 2018-05-11
1462 if (intval($response) == 502) {
1463 $exception = new TemporaryVippsAPIException($msg);
1464 } else {
1465 $exception = new VippsApiException($msg);
1466 }
1467
1468 $exception->responsecode = intval($response);
1469 throw $exception;
1470 }
1471
1472 }
1473