lan_kiosk/dev/fake-swish.php
Anna-Sara Sélea df9f1cf17e Swish feature
2026-08-22 16:13:42 +02:00

129 lines
4.4 KiB
PHP

<?php
/**
* A stand-in for the Swish Handel API, for trying the payment flow locally
* before there is an agreement and a certificate.
*
* It answers the payment request the way Swish does — with a
* PaymentRequestToken header — and then calls the callback URL from the request
* a few seconds later, carrying the callbackIdentifier back as a header just
* like the real one. Everything downstream is real: the receiver app, the
* callback verification, the booking.
*
* The only thing it cannot do is open the Swish app. The token is made up, so
* the app would reject it — on a phone the page simply stays in the waiting
* state until the fake callback lands, which is exactly what we want to see.
*
* Start it inside the kiosk container:
*
* docker exec -d lan_kiosk-api-1 php -S 127.0.0.1:9099 /var/www/html/dev/fake-swish.php
*
* and point the app at it in .env:
*
* SWISH_API_URL=http://127.0.0.1:9099
*
* Amounts that behave differently, so every screen can be reached:
*
* 13 kr → DECLINED, the payer aborted in the app
* 1 kr → no callback at all, the page keeps waiting until it times out
* other → PAID after DELAY_SECONDS
*
* Delete this directory once Swish Handel is in place.
*/
const DELAY_SECONDS = 4;
// Second mode: started in the background by the request below to deliver the
// callback after the payment request has already been answered.
if (PHP_SAPI === 'cli') {
$payload = json_decode(base64_decode($argv[1] ?? ''), true);
if (!$payload) {
exit(1);
}
sleep(DELAY_SECONDS);
$body = json_encode($payload['body']);
$context = stream_context_create([
'http' => [
'method' => 'POST',
'header' => "Content-Type: application/json\r\n"
. 'callbackIdentifier: ' . $payload['identifier'] . "\r\n",
'content' => $body,
'timeout' => 15,
'ignore_errors' => true,
],
]);
$result = @file_get_contents($payload['url'], false, $context);
file_put_contents('/tmp/fake-swish.log', sprintf(
"[%s] callback %s -> %s\n",
date('H:i:s'),
$payload['url'],
$result === false ? 'FAILED' : 'sent'
), FILE_APPEND);
exit(0);
}
// First mode: the payment request itself
$request = json_decode(file_get_contents('php://input'), true) ?: [];
$amount = (float) ($request['amount'] ?? 0);
file_put_contents('/tmp/fake-swish.log', sprintf(
"[%s] %s %s %s\n",
date('H:i:s'),
$_SERVER['REQUEST_METHOD'],
$_SERVER['REQUEST_URI'],
json_encode($request)
), FILE_APPEND);
// Swish rejects a payment request without a payee, and so do we
if (empty($request['payeeAlias'])) {
http_response_code(422);
header('Content-Type: application/json');
echo json_encode([['errorCode' => 'PA02', 'errorMessage' => 'Payee alias is missing']]);
return;
}
$instructionUuid = basename($_SERVER['REQUEST_URI']);
header('PaymentRequestToken: FAKE' . substr($instructionUuid, 0, 12));
header('Location: http://127.0.0.1:9099/swish-cpcapi/api/v1/paymentrequests/' . $instructionUuid);
http_response_code(201);
// 1 kr means "the payer never finishes", so no callback is ever sent
if ((int) $amount === 1) {
return;
}
$callback = [
// Already ends in /kiosk — the app puts it there so the receiver knows where to forward
'url' => $request['callbackUrl'],
'identifier' => $request['callbackIdentifier'] ?? '',
'body' => [
'id' => $instructionUuid,
'payeePaymentReference' => $request['payeePaymentReference'] ?? '',
'paymentReference' => strtoupper(bin2hex(random_bytes(16))),
'payerAlias' => '46701234567',
'payeeAlias' => $request['payeeAlias'],
'amount' => $amount,
'currency' => 'SEK',
'message' => $request['message'] ?? '',
'status' => (int) $amount === 13 ? 'DECLINED' : 'PAID',
'errorCode' => (int) $amount === 13 ? 'BANKIDCL' : null,
'errorMessage' => '',
'dateCreated' => date('c'),
'datePaid' => (int) $amount === 13 ? null : date('c'),
],
];
// Answer first, deliver the callback afterwards — the same order as the real thing
exec(sprintf(
'php %s %s > /dev/null 2>&1 &',
escapeshellarg(__FILE__),
escapeshellarg(base64_encode(json_encode($callback)))
));