dolibarr.bankimport/vendor/nemiah/php-fints/lib/Fhp/Protocol/DialogInitialization.php
data 94efa59df3 v1.7: Multi-invoice payments and payment unlinking
- Add multi-invoice payment support (link one bank transaction to multiple invoices)
- Add payment unlinking feature to correct wrong matches
- Show linked payments, invoices and bank entries in transaction detail view
- Allow linking already paid invoices to bank transactions
- Update README with new features
- Add CHANGELOG.md

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-02-20 09:00:05 +01:00

240 lines
9.6 KiB
PHP
Executable file

<?php
namespace Fhp\Protocol;
use Fhp\BaseAction;
use Fhp\Model\NoPsd2TanMode;
use Fhp\Model\TanMode;
use Fhp\Options\Credentials;
use Fhp\Options\FinTsOptions;
use Fhp\Segment\HISYN\HISYNv4;
use Fhp\Segment\HKIDN\HKIDNv2;
use Fhp\Segment\HKSYN\HKSYNv3;
use Fhp\Segment\HKVVB\HKVVBv3;
use Fhp\Segment\TAN\HKTANFactory;
/**
* Initializes a FinTs dialog. The dialog initialization message is usually the first message that should be sent over
* the wire. The most basic form consists of HIKDN for authentication, and HKVVB to declare the current BPD/UPD versions
* present at the client. The server responds with updated BPD/UPD data.
* @link https://www.hbci-zka.de/dokumente/spezifikation_deutsch/fintsv3/FinTS_3.0_Formals_2017-10-06_final_version.pdf
* Section: C.3
*
* An extended form of dialog initialization (called "synchronization") additionally requests a new Kundensystem-ID by
* sending a HKSYN segment. The Kundensystem-ID identifies the application using the phpFinTS library plus the device on
* which it runs.
* This action automatically executes synchroniziation if `$kundensystemId=null` was passed to the constructor. In this
* case, the opened dialog must not be used for any other (financial/business) actions, so the caller must call
* {@link FinTs::endDialog()} immediately after executing a {@link DialogInitialization} without pre-existing
* Kundensystem-ID.
* @link https://www.hbci-zka.de/dokumente/spezifikation_deutsch/fintsv3/FinTS_3.0_Formals_2017-10-06_final_version.pdf
* Section: C.8
*
* By default (`$hktanRef = 'HKIDN'`), the dialog is opened with strong authentication (PSD2), which requires that the
* $tanMode has already been selected.
* For special PIN/TAN management use cases (e.g. enumerating the available TAN media) that need to be executable
* without strong authentication, or for synchronization (see above), the `$hktanRef` can be set to the segment
* identifier of the PIN/TAN management transaction or null, respectively, to indicate weak authentication.
* @link https://www.hbci-zka.de/dokumente/spezifikation_deutsch/fintsv3/FinTS_3.0_Security_Sicherheitsverfahren_PINTAN_2018-02-23_final_version.pdf
* Section: B.4.3.1 and B.4.3.3
*
* Rough overview of the initialization procedure with no prior information on the client side:
* 1. Open connection.
* 2. Initialize and close anonymously to retrieve BPD (HITANS, HIPINS, ...). This is implemented in
* {@link FinTs::ensureBpdAvailable()}.
* 3. Initialize a dialog with $kundensystemId=null ("synchronization") and $hktanRef=null, and close it again. At this
* point, the allowed TAN modes are available, so the user can select a $tanMode.
* 4. Optional: If the user needs to select a TAN medium, initialize another dialog with $hktanRef=HKTAB to execute
* {@link GetTanMedia}, and close it again. At this point, both $tanMode and $tanMedium are available.
* 4. Initialize a strongly authenticated dialog (which possibly requires a TAN) to retrieve UPD.
* 5. Now we're ready to execute business transactions.
* Note that steps (2.) and (3.) can be skipped if the BPD/Kundensystem-ID are already present on the client side.
*/
class DialogInitialization extends BaseAction
{
// These come from FinTs and are needed as inputs for the dialog initialization. They are NOT available after
// serialization, i.e. not in processResponse().
/** @var FinTsOptions */
private $options;
/** @var Credentials */
private $credentials;
/** @var TanMode|null */
private $tanMode;
/** @var string|null */
private $tanMedium;
/**
* The segment that HKTAN points to. This implicitly defines what kind of dialog is initialized: null means weak
* authentication, 'HKIDN' means strong authentication and other values initialize a special PIN/TAN dialog.
* @var string
*/
private $hktanRef;
// This is the persistent state of the dialog initialization (can be both input and output).
/** @var string|null */
private $kundensystemId; // May be present initially. If not, will send HKSYN to obtain it.
/** @var int|null */
private $messageNumber; // Stored temporarily, to continue properly after TAN input.
/** @var string|null */
private $dialogId; // This is the main result.
// Side results.
/** @var UPD|null */
private $upd;
/**
* @param TanMode|null $tanMode The TAN mode selected by the user.
* @param string|null $tanMedium Possibly a TAN medium selected by the user.
* @param string|null $kundensystemId The current Kundensystem-ID, if the client already has one.
* @param string|null $hktanRef The segment to declare inside HKTAN.
* If this is null, a weakly authenticated dialog (with the TAN mode 999) will be initialized, which can only be
* used for synchronization and/or to retrieve BPD.
* If this is 'HKIDN', a regular, strongly authenticated dialog will be initialized, which may require a TAN.
* If it is one of the special PIN/TAN management segments (e.g. HKTAB), then the dialog does not have strong
* authentication (no TAN required) and can only be used for that one particular transaction.
*/
public function __construct(FinTsOptions $options, Credentials $credentials, ?TanMode $tanMode, ?string $tanMedium, ?string $kundensystemId, ?string $hktanRef = 'HKIDN')
{
if ($hktanRef !== null && $tanMode === null) {
throw new \InvalidArgumentException('hktanRef is ignored unless a tanMode is given');
}
$this->options = $options;
$this->credentials = $credentials;
$this->tanMode = $tanMode instanceof NoPsd2TanMode ? null : $tanMode;
$this->tanMedium = $tanMedium;
$this->kundensystemId = $kundensystemId;
$this->hktanRef = $hktanRef;
}
/**
* @deprecated Beginning from PHP7.4 __unserialize is used for new generated strings, then this method is only used for previously generated strings - remove after May 2023
*/
public function serialize(): string
{
return serialize($this->__serialize());
}
public function __serialize(): array
{
return [
parent::__serialize(),
$this->hktanRef,
$this->kundensystemId,
$this->messageNumber,
$this->dialogId,
];
}
/**
* @deprecated Beginning from PHP7.4 __unserialize is used for new generated strings, then this method is only used for previously generated strings - remove after May 2023
*
* @param string $serialized
* @return void
*/
public function unserialize($serialized)
{
self::__unserialize(unserialize($serialized));
}
public function __unserialize(array $serialized): void
{
list(
$parentSerialized,
$this->hktanRef,
$this->kundensystemId,
$this->messageNumber,
$this->dialogId
) = $serialized;
is_array($parentSerialized) ?
parent::__unserialize($parentSerialized) :
parent::unserialize($parentSerialized);
}
/** {@inheritdoc} */
protected function createRequest(BPD $bpd, ?UPD $upd)
{
throw new \AssertionError('DialogInitialization::createRequest should not be used.');
}
/**
* @param BPD|null $bpd The BPD. Note that we support null here because a dialog initialization is how the BPD can
* be obtained in the first place.
* @param UPD|null $upd The UPD.
* @return array|\Fhp\Segment\BaseSegment|\Fhp\Segment\BaseSegment[]
*/
public function getNextRequest(?BPD $bpd, ?UPD $upd)
{
$request = [
HKIDNv2::create($this->options->bankCode, $this->credentials, $this->kundensystemId ?? '0'),
HKVVBv3::create($this->options, $bpd, $upd),
];
if ($this->tanMode !== null) {
$request[] = HKTANFactory::createProzessvariante2Step1(
$this->tanMode, $this->tanMedium, $this->hktanRef ?? 'HKIDN');
}
if ($this->kundensystemId === null) {
// NOTE: HKSYN must be *after* HKTAN.
$request[] = HKSYNv3::createEmpty(); // See section C.8.1.1
}
return $request;
}
public function processResponse(Message $response)
{
parent::processResponse($response);
$this->dialogId = $response->header->dialogId;
if ($this->kundensystemId === null) {
/** @var HISYNv4 $hisyn */
$hisyn = $response->requireSegment(HISYNv4::class);
if ($hisyn->kundensystemId === null) {
throw new UnexpectedResponseException('No Kundensystem-ID received');
}
$this->kundensystemId = $hisyn->kundensystemId;
}
if (UPD::containedInResponse($response)) {
$this->upd = UPD::extractFromResponse($response);
}
}
public function isStronglyAuthenticated(): bool
{
return $this->hktanRef === 'HKIDN';
}
public function getKundensystemId(): ?string
{
return $this->kundensystemId;
}
public function getMessageNumber(): ?int
{
return $this->messageNumber;
}
public function setMessageNumber(?int $messageNumber): void
{
$this->messageNumber = $messageNumber;
}
public function getDialogId(): ?string
{
return $this->dialogId;
}
/**
* To be called when a TAN is required for login, but we need to intermittently store the Dialog-ID.
*/
public function setDialogId(?string $dialogId): void
{
$this->dialogId = $dialogId;
}
public function getUpd(): ?UPD
{
return $this->upd;
}
}