feat: provision a teamer from a received application
This commit is contained in:
1 parent
d5f4b12f0a
commit
b530679658
23 files changed
+2433
-148
No files matched your search
@@ -7,6 +7,8 @@ use App\BusProNet\Model\CrmAttributesResponse;
|
||||
use App\BusProNet\Model\NotificationResponse;
|
||||
use App\BusProNet\Model\ProfileResponse;
|
||||
use App\BusProNet\Model\ProfileUpdateResponse;
|
||||
use App\BusProNet\Model\RegistrationResponse;
|
||||
use App\Entity\ApplicationFormSubmission;
|
||||
use App\Entity\User;
|
||||
use League\Flysystem\FilesystemException;
|
||||
use League\Flysystem\FilesystemOperator;
|
||||
@@ -83,6 +85,86 @@ class ApiClient
|
||||
return $this->sendRequest(static::TYPE_CUSTOMER_DATA, $data);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates the applicant's address in the CRM, which is what makes them a person BusPro knows.
|
||||
*
|
||||
* BusPro answers with the ids of an address it already held when it recognises the person
|
||||
* ("neuanlage" False), so this never produces a duplicate and needs no pre-flight lookup -
|
||||
* reading an address would require the customer's password, which provisioning deliberately
|
||||
* does not have.
|
||||
*
|
||||
* "ohnemailversand" suppresses BusPro's own mail about the new record: the applicant is asked
|
||||
* to set a password by resetPassword() at the end of provisioning instead, so that a failure
|
||||
* in between never mails somebody who has not been provisioned.
|
||||
*
|
||||
* @throws ApiClientException
|
||||
* @throws ResponseParserException
|
||||
*/
|
||||
public function createAddress(ApplicationFormSubmission $submission): NotificationResponse|RegistrationResponse
|
||||
{
|
||||
$data = [
|
||||
'anfrage' => [
|
||||
'user' => $this->config['bpn_username'],
|
||||
'key' => $this->createKey($this->config['bpn_username'], $this->config['bpn_password'], static::TYPE_CUSTOMER_DATA),
|
||||
'satz' => ['@typ' => static::TYPE_CUSTOMER_DATA],
|
||||
'art' => 'Adresse_Neu',
|
||||
'adressdaten' => $submission->toPayload(),
|
||||
'ohnemailversand' => 'True',
|
||||
],
|
||||
];
|
||||
|
||||
return $this->sendRequest(static::TYPE_CUSTOMER_DATA, $data);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets or clears one CRM selection on a person - the one write to the CRM this application
|
||||
* makes. Everything else about selections is read-only here: roles and hotel codes are synced
|
||||
* from BusPro on every login and an admin may approve a claim, never grant one.
|
||||
*
|
||||
* "login" switches the password check off, which only works when the request identifies the
|
||||
* person by id: both ids are required, and the person id is "idadresseperson" on the way in
|
||||
* although the response calls it "idperson". The element order and the single-selection
|
||||
* payload are what BusPro was verified to accept - the art is undocumented, see
|
||||
* .ddev/plans/buspro-crm-selection-write.md before changing the shape.
|
||||
*
|
||||
* Idempotent: setting a selection that is already set answers änderung=True and changes
|
||||
* nothing, so a caller may write unconditionally and take isUpdated() as the success check.
|
||||
*
|
||||
* The write only goes through where the selection is marked changeable in the BusPro
|
||||
* settings, and that flag is per environment - 1070 is writable in production only.
|
||||
*
|
||||
* @throws ApiClientException
|
||||
* @throws ResponseParserException
|
||||
*/
|
||||
public function updateCrmAttribute(
|
||||
string $email,
|
||||
int $addressId,
|
||||
int $personId,
|
||||
int $selectionId,
|
||||
bool $selected,
|
||||
): NotificationResponse|ProfileUpdateResponse {
|
||||
$data = [
|
||||
'anfrage' => [
|
||||
'user' => $this->config['bpn_username'],
|
||||
'key' => $this->createKey($this->config['bpn_username'], $this->config['bpn_password'], static::TYPE_CUSTOMER_DATA),
|
||||
'satz' => ['@typ' => static::TYPE_CUSTOMER_DATA],
|
||||
'art' => 'SelektionCRM_Ändern',
|
||||
'login' => 'J',
|
||||
'email' => $email,
|
||||
'idadresse' => $addressId,
|
||||
'idadresseperson' => $personId,
|
||||
'selektionsmerkmale' => [
|
||||
'selektion' => [
|
||||
// Capitalised as in the read response, which is what the write accepts.
|
||||
['@id' => $selectionId, '@auswahl' => true === $selected ? 'True' : 'False'],
|
||||
],
|
||||
],
|
||||
],
|
||||
];
|
||||
|
||||
return $this->sendRequest(static::TYPE_CUSTOMER_DATA, $data);
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws ApiClientException
|
||||
* @throws ResponseParserException
|
||||
|
||||
@@ -0,0 +1,37 @@
|
||||
<?php
|
||||
|
||||
namespace App\BusProNet\Model;
|
||||
|
||||
/**
|
||||
* The answer to "Adresse_Neu": the ids of the address the CRM now holds for this person, and
|
||||
* whether it had to create it.
|
||||
*
|
||||
* isNewRecord() is BusPro's own duplicate check ("neuanlage"): False means it matched an address
|
||||
* that already existed and answered with that one's ids, so a caller never has to look for
|
||||
* duplicates itself - which it could not anyway, as reading an address needs the customer's
|
||||
* password. It says nothing about which CRM selections that address carries.
|
||||
*/
|
||||
class RegistrationResponse
|
||||
{
|
||||
public function __construct(
|
||||
private readonly int $addressId,
|
||||
private readonly int $personId,
|
||||
private readonly bool $isNewRecord,
|
||||
) {
|
||||
}
|
||||
|
||||
public function getAddressId(): int
|
||||
{
|
||||
return $this->addressId;
|
||||
}
|
||||
|
||||
public function getPersonId(): int
|
||||
{
|
||||
return $this->personId;
|
||||
}
|
||||
|
||||
public function isNewRecord(): bool
|
||||
{
|
||||
return $this->isNewRecord;
|
||||
}
|
||||
}
|
||||
@@ -14,6 +14,7 @@ use App\BusProNet\Model\NotificationResponse;
|
||||
use App\BusProNet\Model\Pickup;
|
||||
use App\BusProNet\Model\ProfileResponse;
|
||||
use App\BusProNet\Model\ProfileUpdateResponse;
|
||||
use App\BusProNet\Model\RegistrationResponse;
|
||||
use Symfony\Component\OptionsResolver\OptionsResolver;
|
||||
|
||||
class ResponseParser
|
||||
@@ -50,8 +51,17 @@ class ResponseParser
|
||||
case 'Adressdaten_Ändern':
|
||||
return $this->createProfileUpdateResponse($xml);
|
||||
case 'SelektionCRM':
|
||||
case 'SelektionCRM_Ändern':
|
||||
return $this->createCrmAttributesResponse($xml);
|
||||
case 'SelektionCRM_Ändern':
|
||||
// Not a CRM attribute tree: the write answers idadresse + idperson +
|
||||
// änderung, exactly the shape createProfileUpdateResponse() reads. It used
|
||||
// to be routed to createCrmAttributesResponse(), which walks a
|
||||
// "selektionsmerkmale" node that is absent here and would have returned an
|
||||
// empty response with every role flag false - dead code until something
|
||||
// sent the art (.ddev/plans/buspro-crm-selection-write.md).
|
||||
return $this->createProfileUpdateResponse($xml);
|
||||
case 'Adresse_Neu':
|
||||
return $this->createRegistrationResponse($xml);
|
||||
}
|
||||
break;
|
||||
case ApiClient::TYPE_BASE_DATA_COUNTRIES:
|
||||
@@ -166,6 +176,34 @@ class ResponseParser
|
||||
return $response;
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws ResponseParserException
|
||||
*/
|
||||
public function createRegistrationResponse(\SimpleXMLElement $xml): RegistrationResponse
|
||||
{
|
||||
$addressIdXml = $xml->xpath('idadresse') ?: [];
|
||||
$personIdXml = $xml->xpath('idperson') ?: [];
|
||||
|
||||
// Without both ids there is nothing to carry on with: the CRM selection is set by id, and
|
||||
// a local account without them cannot be matched again on the next login. Better a parse
|
||||
// error naming the response than a half-provisioned applicant.
|
||||
if (0 === count($addressIdXml) || 0 === count($personIdXml)) {
|
||||
throw new ResponseParserException('Missing idadresse or idperson in the Adresse_Neu response');
|
||||
}
|
||||
|
||||
$isNewRecord = false;
|
||||
$isNewRecordXml = $xml->xpath('neuanlage') ?: [];
|
||||
if (0 < count($isNewRecordXml)) {
|
||||
$isNewRecord = 'true' === strtolower((string) $isNewRecordXml[0]);
|
||||
}
|
||||
|
||||
return new RegistrationResponse(
|
||||
(int) $addressIdXml[0],
|
||||
(int) $personIdXml[0],
|
||||
$isNewRecord,
|
||||
);
|
||||
}
|
||||
|
||||
public function createCrmAttributesResponse(\SimpleXMLElement $xml): CrmAttributesResponse
|
||||
{
|
||||
$groups = [];
|
||||
|
||||
Reference in new issue
Block a user