# Example code

The code for this domain module can be found in:

```
/custom/modules/HwCpsDatensysteme/Domain/HwCpsDatensystemeDomainHandler
```

For each domain module, you have a php class, for example the HwCpsDatensystemeDomainHandler.php.

## Example (CPS Datensysteme)

```php
<?php

namespace custom\modules\HwCpsDatensysteme\Domain;


use App\Enums\Domain\ContactSexEnum;
use App\Enums\Domain\ContactTypeEnum;
use App\Exceptions\CouldNotCreateDomainContact;
use App\Exceptions\InvalidAuthInfo;
use App\Exceptions\StorefrontException;
use App\Framework\Core\ConfigService;
use App\Framework\Core\Hosting\Domain\DnsRecordItem;
use App\Framework\Core\Hosting\Domain\DomainHandlerAbstract;
use App\Framework\Core\Hosting\Domain\DomainHandlerInterface;
use App\Framework\Core\Hosting\Domain\DomainSyncItem;
use App\Framework\Core\Hosting\Domain\PriceSyncItem;
use App\Framework\Core\Hosting\Domain\Responses\DomainAuthcodeResponse;
use App\Framework\Core\Hosting\Domain\Responses\DomainCheckResponse;
use App\Framework\Core\Hosting\Domain\Responses\DomainDeletedResponse;
use App\Framework\Core\Hosting\Domain\Responses\DomainNameserverResponse;
use App\Framework\Core\Hosting\Domain\Responses\DomainRegisteredResponse;
use App\Framework\Core\Hosting\Domain\Responses\DomainSyncResponse;
use App\Framework\Core\Hosting\Domain\Responses\DomainSyncStatusEnum;
use App\Framework\Core\Hosting\Domain\Responses\DomainTransferredResponse;
use App\Framework\Core\Hosting\Domain\Responses\DomainTransferSyncResponse;
use App\Framework\Core\Hosting\Domain\Whois\WhoisService;
use App\Models\Domain\Domain;
use App\Models\Domain\DomainContact;
use Carbon\Carbon;

use Exception;
use custom\modules\HwCpsDatensysteme\Domain\lib\ApiClient;
use Illuminate\Support\Collection;
use Illuminate\Support\Facades\Cache;
use Illuminate\Support\Facades\Log;
use Illuminate\Support\Str;
use libphonenumber\PhoneNumberFormat;
use libphonenumber\PhoneNumberUtil;
use Throwable;

class HwCpsDatensystemeDomainHandler extends DomainHandlerAbstract implements DomainHandlerInterface {
	public static $cps;
	public bool $supportsHandleFilter = true;
	public bool $supportsDnsPriorityField = true;

	public bool $cacheGetDomainResponse = false;

	public function __construct(?int $salesChannelId = null) {
		parent::__construct($salesChannelId);
	}


	public array $availableTypes = [
		'A' => 'A',
		'AAAA' => 'AAAA',
		'CAA' => 'CAA',
		'CNAME' => 'CNAME',
		'MX' => 'MX',
		'SRV' => 'SRV',
		'TXT' => 'TXT'
	];

	public function checkDomain(string $sld, string $tld): ?DomainCheckResponse
	{
		try {

			// Cache for 1 Hour
			$response = Cache::remember('hwcps_rdap_check_' . $sld . '.' . $tld, 1 * 60 * 60, function () use ($sld, $tld) {

				$ch = curl_init("https://rdap.cps-datensysteme.de/check/{$sld}.{$tld}");

				curl_setopt_array($ch, [
					CURLOPT_RETURNTRANSFER => true,
					CURLOPT_USERPWD => $this->config['rdap_user'] . ':' . $this->config['rdap_password'],
					CURLOPT_HTTPAUTH => CURLAUTH_BASIC,
					CURLOPT_TIMEOUT => 30,
				]);

				return json_decode(curl_exec($ch));
			});

			return new DomainCheckResponse(
				$response->is_available,
				null,
				"HwCpsDatensysteme_RDAP"
			);
		} catch (Exception|Throwable $e) {
			$response = app(WhoisService::class)->checkDomain($sld, $tld);
			$response->checker = "WhoisService_Backup";

			return $response;
		}
	}

	public function getPrices(): array {

		$xml = <<<XML
<?xml version="1.0" encoding="utf-8" ?>
<request>
	{$this->getXmlHeader()}
    <transaction>
        <report>
            <group>account</group>
            <data_type>csv</data_type>
        </report>
    </transaction>
</request>
XML;

		$explodedresponse = Cache::remember('hwcps_prices', 5 * 60, function () use ($xml) {
			$response = $this->getClient()->callreportRaw($xml, $this->config['sandbox']);
			return explode("\n", $response->result->detail->report->pricing->data);
		});

		$priceSyncItems = [];
		$priceitems = [];

		foreach ($explodedresponse as $singlePrice) {

			$values = explode(";", $singlePrice);
			if ($values[0] == "") continue;

			$tldstring = explode(".", $values[3]);
			$tld = strtolower($tldstring[1] ?? "");

			if (!isset($priceitems[$tld])) {
				$priceitems[$tld] = [];
				$priceitems[$tld]['tld'] = $tld;
			}


			if ($values[10] == "create" && $values[11] == "domain") {
				$priceitems[$tld]['create'] = $values[16];
			}
			if ($values[10] == "renew" && $values[11] == "domain") {
				$priceitems[$tld]['renew'] = $values[16];
			}
			if ($values[10] == "transfer" && $values[11] == "domain") {
				$priceitems[$tld]['transfer'] = $values[16];
			}
			if ($values[10] == "create" && $values[11] == "restore") {
				$priceitems[$tld]['restore'] = $values[16];
			}
			if ($values[10] == "modify" && $values[11] == "domain") {
				$priceitems[$tld]['modify'] = $values[16];
			}

		}

		foreach ($priceitems as $singlePriceItem) {
			if (str_contains($singlePriceItem['tld'], " ") || $singlePriceItem['tld'] === "") {
				continue;
			}

			$priceSyncItems[] = new PriceSyncItem(
				$singlePriceItem['tld'],
				$singlePriceItem['create'] ?? 0,
				$singlePriceItem['renew'] ?? 0,
				$singlePriceItem['transfer'] ?? 0,
				0,
				$singlePriceItem['modify'] ?? 0,
				$singlePriceItem['restore'] ?? 0,
			);
		}


		return $priceSyncItems;
	}

	public function syncDomains(Collection $domains): array {
		$responseData = [];

		$xml = <<<XML
<?xml version="1.0" encoding="utf-8" ?>
<request>
    	{$this->getXmlHeader()}
    <transaction>
        <report>
            <group>domain</group>
            <data_type>csv</data_type>
        </report>
    </transaction>
</request>

XML;


		$response = $this->getClient()->callreportRaw($xml, $this->config['sandbox']);
		$explode = explode("\n", $response->result->detail->report->domain->data);

		foreach ($explode as $domainItem) {
			$domainItemExploded = explode(';', $domainItem);
			if ($domainItemExploded[0] == 'domain') continue;
			if ($domainItemExploded[0] == '') continue;

			$allDomainsStructured[strtolower($domainItemExploded[0])] = $domainItemExploded;
		}

		foreach ($domains as $domain) {
			$resItem = $allDomainsStructured[strtolower($domain->fqdn)] ?? $allDomainsStructured[strtolower($domain->punycode_fqdn)] ?? null;

			if ($resItem === null) {
				$responseData[$domain->id] = new DomainSyncResponse(DomainSyncStatusEnum::INACTIVE);
				continue;
			}

//			dump($resItem);

			// DNS sec active:
//			$resItem[30] !== "not found"

			$responseData[$domain->id] = new DomainSyncResponse(
				DomainSyncStatusEnum::ACTIVE,
				$resItem[16] === "active",
				Carbon::parse($resItem[10]),
				Carbon::parse($resItem[7]),
				$resItem[14] === "active",
			);
		}

//		dd($responseData);

		return $responseData;
	}

	public function register(Domain $domain, array $nameservers, DomainContact $ownerC, DomainContact $adminC, DomainContact $techC, DomainContact $zoneC): DomainRegisteredResponse {

		/**
		 * Nameservers.
		 */
		$ns[] = $nameservers[0] ?? "";
		$ns[] = $nameservers[1] ?? "";
		$ns[] = $nameservers[2] ?? "";
		$ns[] = $nameservers[3] ?? "";
		$ns[] = $nameservers[4] ?? "";
		$nsstring = '';

		foreach ($ns as $value) {
			if ($value != '') {
				$nsstring .= '<dns>
		<hostname>' . $value . '</hostname>
            </dns>';
			}
		}

		// get the actual handles from the contacts
		$ownerC = $this->getDomainHandle($ownerC);
		$adminC = $this->getDomainHandle($adminC);
		$techC = $this->getDomainHandle($techC);
		$zoneC = $this->getDomainHandle($zoneC);

		$xml = <<<XML
<?xml version="1.0" encoding="utf-8" ?>
<request>
	{$this->getXmlHeader()}
    <transaction>
		<group>domain</group>
		<action>create</action>
		<attribute>domain</attribute>
		<object>{$domain->punycode_fqdn}</object>
		<values>
			{$nsstring}
			<adminc>{$adminC}</adminc>
			<techc>{$techC}</techc>
			<billc>{$zoneC}</billc>
			<ownerc>{$ownerC}</ownerc>
		</values>
	</transaction>
</request>
XML;


		try {
			$response = $this->getClient()->callRaw($xml, $this->config['sandbox']);
		} catch (Exception $e) {
			if(str_contains($e->getMessage(), "domain already maintained by registrar")) {
				// Handle as registered
				return new DomainRegisteredResponse($domain->fqdn);
			}

			throw $e;
		}
//        dd($response);

		if ($response->result->code != "1000") {
			throw new Exception("Got API error: " . implode(", ", $response->result->message));
		}

		return new DomainRegisteredResponse(
			$domain->sld . '.' . $domain->tld,
		);
	}

	public function createZone(Domain $domain, array $nameservers) {

		$xml = <<<XML
<?xml version="1.0" encoding="utf-8" ?>
<request>
	{$this->getXmlHeader()}
    <transaction>
		<group>dns</group>
		<action>create</action>
		<attribute>managed</attribute>
		<object>{$domain->punycode_fqdn}</object>
		<values>
			{$this->_getZoneXml($domain)}
		</values>
	</transaction>
</request>
XML;

		$this->getClient()->callRaw($xml, $this->config['sandbox']);
	}

	private function _getZoneXml(Domain $domain) {
		$primaryNs = $domain->nameservers[0];
		$nsstring = "";

		foreach (array_filter($domain->nameservers) as $value) {
			$nsstring .= '<ns>' . $value . '</ns>';
		}

		$defaultTtl = 600;

		try {
			$defaultTtl = app(ConfigService::class)->get('core.domain.default_ttl', $domain->sales_channel_id, 600);
		} catch (Exception $e) {

		}

		return "{$nsstring}
			<soa>
				<ttl>{$defaultTtl}</ttl>
				<primary_ns>{$primaryNs}</primary_ns>
			</soa>";
	}

	public function transfer(Domain $domain, array $nameservers, DomainContact $ownerC, DomainContact $adminC, DomainContact $techC, DomainContact $zoneC): DomainTransferredResponse {

//		throw new Exception("test");
		/**
		 * Nameservers.
		 */
		$ns[] = $nameservers[0] ?? "";
		$ns[] = $nameservers[1] ?? "";
		$ns[] = $nameservers[2] ?? "";
		$ns[] = $nameservers[3] ?? "";
		$ns[] = $nameservers[4] ?? "";
		$nsstring = '';

		foreach ($ns as $value) {
			if ($value != '') {
				$nsstring .= '<dns>
		<hostname>' . $value . '</hostname>
            </dns>';
			}
		}

		// get the actual handles from the contacts
		$ownerC = $this->getDomainHandle($ownerC);
		$adminC = $this->getDomainHandle($adminC);
		$techC = $this->getDomainHandle($techC);
		$zoneC = $this->getDomainHandle($zoneC);

		$auth = htmlspecialchars($domain->transfer_authcode);

		$xml = <<<XML
<?xml version="1.0" encoding="utf-8" ?>
<request>
	{$this->getXmlHeader()}
    <transaction>
		<group>domain</group>
		<action>transfer</action>
		<attribute>domain</attribute>
		<object>{$domain->punycode_fqdn}</object>
		<values>
			{$nsstring}
			<adminc>{$adminC}</adminc>
			<techc>{$techC}</techc>
			<billc>{$zoneC}</billc>
			<ownerc>{$ownerC}</ownerc>
			<auth_info>{$auth}</auth_info>
		</values>
	</transaction>
</request>
XML;


		try  {
			$response = $this->getClient()->callRaw($xml, $this->config['sandbox']);
		} catch (Exception $e) {
			if (str_contains($e->getMessage(), "AuthInfo does not match")) {
				throw new InvalidAuthInfo();
			}

			throw $e;
		}

		Log::debug("CPS transfer response", [
			'response' => (array)$response,
			"message" => (string)$response->result->message,
			"code" => (string)$response->result->code,
		]);

		if (str_contains((string)$response->result->message, "AuthInfo does not match")) {
			throw new InvalidAuthInfo();
		}

		if (!in_array((string)$response->result->code, ["1000", "1001"])) {
			throw new Exception("Got API error: " . implode(", ", $response->result->message));
		}

		return new DomainTransferredResponse(
			$domain->sld . '.' . $domain->tld,
			null,
			strtotime('+1 year', strtotime($response->transaction->created)),
		);
	}


	public function syncTransfer(Domain $domain): DomainTransferSyncResponse {
		$xml = <<<XML
<?xml version="1.0" encoding="utf-8" ?>
<request>
	{$this->getXmlHeader()}
    <transaction>
        <group>domain</group>
        <action>info</action>
        <attribute>domain</attribute>
        <object>{$domain->punycode_fqdn}</object>
    </transaction>
</request>
XML;

		try {
			$response = $this->getClient()->callRaw($xml, $this->config['sandbox']);
		} catch (Exception $e) {
			return new DomainTransferSyncResponse(
				false,
				null,
				true,
				$e->getMessage()
			);
		}

		/**
		 * Transfer is still pending
		 */
		try {
			if (((array)$response->result->detail->values->transaction_lock_status)[0] === "pendingTransfer") {
				return new DomainTransferSyncResponse(
					false
				);
			}
		} catch (Throwable $e) {

		}

		return new DomainTransferSyncResponse(
			true,
			Carbon::parse($response->expire),
		);
	}

	public function getAuthcode(Domain $domain): DomainAuthcodeResponse {

		$xml = <<<XML
<?xml version="1.0" encoding="utf-8" ?>
<request>
	{$this->getXmlHeader()}
    <transaction>
        <group>domain</group>
        <action>info</action>
        <attribute>domain</attribute>
        <object>{$domain->punycode_fqdn}</object>
    </transaction>
</request>
XML;


		$response = $this->getClient()->callRaw($xml, $this->config['sandbox']);

		$eppCode = (string)$response->result->detail->values->auth_info;
		$transferLock = (string)$response->result->detail->values->transfer_lock;

		if ($eppCode == '' && $transferLock == 'disabled') {
			throw new StorefrontException('Authcode is not available for this domain');
		} elseif ($transferLock == 'active') {
			throw new StorefrontException('Transferlock is active. Please disable to view the authcode.');
		}

		return new DomainAuthcodeResponse($eppCode);
	}


	public function setTransferLock(Domain $domain, bool $transferLockEnabled) {

		$status = $transferLockEnabled ? "active" : "disabled";

		$xml = <<<XML
<?xml version="1.0" encoding="utf-8" ?>
<request>
	{$this->getXmlHeader()}
    <transaction>
        <group>domain</group>
        <action>modify</action>
        <attribute>transfer_lock</attribute>
        <object>{$domain->punycode_fqdn}</object>
		<values>
			<status>{$status}</status>
        </values>
    </transaction>
</request>
XML;


		$this->getClient()->callRaw($xml, $this->config['sandbox']);
	}

	public function delete(Domain $domain): DomainDeletedResponse {
		$postfields = [
			'customerNumber' => $this->config['cnumber'],
			'password' => $this->config['password'],
			'testMode' => $this->config['sandbox'] ? 'on' : 'off',
			'user' => $this->config['user'],
			'domain' => $domain->sld . '.' . $domain->tld,
		];

		try {
			$response = $this->getClient()->call('domainDelete', $postfields);
		} catch (Exception|Throwable $e) {
			if (!$this->shouldTreatDeleteNotActiveAsSuccess($e)) {
				throw $e;
			}
		}

		try {

			$xml = <<<XML
<?xml version="1.0" encoding="utf-8" ?>
<request>
	{$this->getXmlHeader()}
    <transaction>
		<group>dns</group>
		<action>delete</action>
		<attribute>managed</attribute>
		<object>{$domain->punycode_fqdn}</object>
	</transaction>
</request>
XML;

			$this->getClient()->callRaw($xml, $this->config['sandbox']);
		} catch (Exception|Throwable $e) {
			// Silence
		}

		return new DomainDeletedResponse();
	}

	private function shouldTreatDeleteNotActiveAsSuccess(Throwable $e): bool
	{
		if (empty($this->config['deleteNotActiveAsSuccess'])) {
			return false;
		}

		$message = $e->getMessage();

		return Str::contains($message, [
			'domain not active within this user account',
			'Die Domain wird nicht vom anfragenden User verwaltet oder existiert nicht',
		]);
	}


	public function getDns(Domain $domain): ?array {
		try {

			$xml = <<<XML
<?xml version="1.0" encoding="utf-8" ?>
<request>
	{$this->getXmlHeader()}
    <transaction>
        <group>dns</group>
        <action>info</action>
        <attribute>dnszone_records</attribute>
        <object>{$domain->punycode_fqdn}</object>
    </transaction>
</request>
XML;


			$response = $this->getClient()->callRaw($xml, $this->config['sandbox']);
		} catch (Exception $e) {
			if (config('app.debug')) throw $e;

			Log::error($e);
			return null;
		}

		$index = 0;
		$output = [];

		foreach ((array)$response->result->detail->values as $type => $items) {
			$items = (array)$items;

			if (!in_array(strtoupper($type), $this->availableTypes)) {
				continue;
			}

			if (isset($items['rr_value'])) {
				$items = [$items];
			}

			foreach ($items as $item) {
				$item = (array)$item;

				$output[$index] = new DnsRecordItem(
					$index,
					strtoupper($type),
					$item['rr_name'],
					$item['rr_value'],
					null,
					$item['rr_preference'] ?? null,
				);

				$index++;
			}
		}

		return $output;
	}

	public function saveDns(Domain $domain, array $records) {

		$recordString = "";

		/** @var DnsRecordItem $recordItem */
		foreach ($records as $recordItem) {
			$type = strtolower($recordItem->type);
			$priority = "";

			if($recordItem->type === "MX" || $recordItem->type === "SRV") {
				$priority = "<rr_preference>{$recordItem->priority}</rr_preference>";
			}

			$preference = "";

			$recordString .= "<{$type}>
									<rr_name>{$recordItem->hostname}</rr_name>
									<rr_value>{$recordItem->content}</rr_value>
									{$priority}
								</{$type}>";
		}

		$recordString = trim(preg_replace('/[\t\r\n]/', '', $recordString));

		$xml = <<<XML
<?xml version="1.0" encoding="utf-8" ?>
<request>
	{$this->getXmlHeader()}
    <transaction>
        <group>dns</group>
        <action>modify</action>
        <attribute>managed</attribute>
        <object>{$domain->punycode_fqdn}</object>
        <values>
        	{$this->_getZoneXml($domain)}
        	{$recordString}
        </values>
    </transaction>
</request>
XML;


//		try {
			$this->getClient()->callRaw($xml, $this->config['sandbox']);
//		} catch (Exception $e) {
//			dd($e->getMessage());
//		}
	}

	public function deleteDns(Domain $domain, DnsRecordItem $record) {

		$currentDns = $this->getDns($domain);
		$wasFound = false;

		foreach ($currentDns as $index => $currentDnsRecord) {
			if($currentDnsRecord->type === $record->type && $currentDnsRecord->hostname === $record->hostname) {
				$wasFound = true;
				unset($currentDns[$index]);
			}
		}

		if(!$wasFound) {
			throw new Exception("hostware-IX: Could not determine the original dns record. Please contact support!");
		}

		$this->saveDns($domain, $currentDns);
	}

	public function createDns(Domain $domain, DnsRecordItem $record, array $multipleRecords = []) {
		$currentDns = $this->getDns($domain);


		if(count($multipleRecords) > 0) {

			/**
			 * Special Treatment for CPS rate limit for DNS templates
			 */
			foreach ($multipleRecords as $additionalRecord) {
				$currentDns[] = $additionalRecord;
			}
		} else {

			/**
			 * Add and save the record to the existing ones
			 */
			$currentDns[] = $record;
		}

		$this->saveDns($domain, $currentDns);
	}

	public function updateDns(Domain $domain, DnsRecordItem $originalRecord, DnsRecordItem $updatedRecord) {

		$currentDns = $this->getDns($domain);
		$wasFound = false;

		foreach ($currentDns as $index => $currentDnsRecord) {
			if($currentDnsRecord->type === $originalRecord->type && $currentDnsRecord->hostname === $originalRecord->hostname) {
				$wasFound = true;
				unset($currentDns[$index]);
			}
		}

		if(!$wasFound) {
			throw new Exception("hostware-IX: Could not determine the original dns record. Please contact support!");
		}

		$currentDns[] = $updatedRecord;
		$this->saveDns($domain, $currentDns);
	}

	public function getNameservers(Domain $domain): DomainNameserverResponse {
		$xml = <<<XML
<?xml version="1.0" encoding="utf-8" ?>
<request>
	{$this->getXmlHeader()}
	<transaction>
		<group>domain</group>
		<action>info</action>
		<attribute>domain</attribute>
		<object>{$domain->punycode_fqdn}</object>
	</transaction>
</request>
XML;

		$response = $this->getClient()->callRaw($xml, $this->config['sandbox']);

		return new DomainNameserverResponse(
			$response->result->detail->values->dns[0]->hostname,
			$response->result->detail->values->dns[1]->hostname,
			$response->result->detail->values->dns[2]->hostname,
			$response->result->detail->values->dns[3]->hostname,
			$response->result->detail->values->dns[4]->hostname
		);
	}

	private function _getNameservers(array $nameservers) {
		$dnsString = '';

		foreach ($nameservers as $value) {
			if ($value != '') {
				$dnsString .= '<dns><hostname>' . $value . '</hostname></dns>
';
			}
		}

		return $dnsString;
	}

	public function saveNameservers(Domain $domain, array $nameservers) {
		$dnsString = $this->_getNameservers($nameservers);

		$postfields = [
			'customerNumber' => $this->config['cnumber'],
			'password' => $this->config['password'],
			'testMode' => $this->config['sandbox'] ? 'on' : 'off',
			'user' => $this->config['user'],
			'domain' => $domain->punycode_fqdn,
			'ns' => $dnsString
		];

		$response = $this->getClient()->call('domainInfo', $postfields);

		$adminC = (string)$response->result->detail->values->adminc;
		$billc = (string)$response->result->detail->values->billc;
		$ownerc = (string)$response->result->detail->values->ownerc;
		$techc = (string)$response->result->detail->values->techc;

		$postfields['adminc'] = $adminC;
		$postfields['billc'] = $billc;
		$postfields['ownerc'] = $ownerc;
		$postfields['techc'] = $techc;

		$this->getClient()->call('changeNameservers', $postfields);
	}


	public function updateDomainHandles(Domain $domain, DomainContact $ownerC, DomainContact $adminC, DomainContact $techC, DomainContact $zoneC) {

		// get the actual handles from the contacts
		$ownerC = $this->getDomainHandle($ownerC);
		$adminC = $this->getDomainHandle($adminC);
		$techC = $this->getDomainHandle($techC);
		$zoneC = $this->getDomainHandle($zoneC);

		$xml = <<<XML
<?xml version="1.0" encoding="utf-8" ?>
<request>
	{$this->getXmlHeader()}
    <transaction>
		<group>domain</group>
		<action>modify</action>
		<attribute>domain</attribute>
		<object>{$domain->punycode_fqdn}</object>
		<values>
			<adminc>{$adminC}</adminc>
			<techc>{$techC}</techc>
			<billc>{$zoneC}</billc>
			<ownerc>{$ownerC}</ownerc>
			{$this->_getNameservers($domain->nameservers)}
		</values>
	</transaction>
</request>
XML;

//		dd($xml);


		$response = $this->getClient()->callRaw($xml, $this->config['sandbox']);

		if ($response->result->code != "1000") {
			throw new Exception("Got API error: " . implode(", ", $response->result->message));
		}
	}

	public function getDomainPageSize(): ?int {
		return 250;
	}

	public function getDomains(): ?array {
//		return [];
//		dd($this->getDomainsOwnerHandle);

		$postfields = [
			'customerNumber' => $this->config['cnumber'],
			'password' => $this->config['password'],
			'testMode' => $this->config['sandbox'] ? 'on' : 'off',
			'user' => $this->config['user'],
		];

		$explode = Cache::remember("hw-CPS-listDomains", 60 * 5, function () use ($postfields) {
			$response = $this->getClient()->callreport('listDomains', $postfields);
			return explode("\n", $response->result->detail->report->domain->data);
		});



		$this->getDomainsTotalCount = count($explode);
		$explode = array_slice($explode, ($this->getDomainsPage - 1) * $this->getDomainPageSize(), $this->getDomainPageSize());

		/**
		 * Get all domain handles
		 */
		$xml = <<<XML
<?xml version="1.0" encoding="utf-8" ?>
<request>
	{$this->getXmlHeader()}
    <transaction>
        <report>
            <group>contact</group>
            <data_type>csv</data_type>
        </report>
    </transaction>
</request>
XML;

		$explodedresponse = Cache::remember("hw-CPS-contactCsv", 60 * 5, function () use ($xml) {
			$response = $this->getClient()->callreportRaw($xml, $this->config['sandbox']);
			return explode("\n", $response->result->detail->report->contact->data);
		});

		$contacts = collect([]);

		foreach ($explodedresponse as $lineItem) {
			$lineItem = explode(";", $lineItem);
			if($lineItem[0] === "cid") continue;
			if(count($lineItem) !== 16) continue;

//			dd($lineItem);
			$contacts->add($this->_getContactFromHandle($lineItem));
		}

		$contacts = $contacts->keyBy(function ($item) {
			return (string)$item->getAttributes()['id'];
		});


		$output = [];

		foreach ($explode as $domainItem) {
			$domainItemExploded = explode(';', $domainItem);

			if ($domainItemExploded[0] == 'domain') continue;
			if ($domainItemExploded[0] == '') continue;

			if($this->getDomainsOwnerHandle !== null && $domainItemExploded[20] !== $this->getDomainsOwnerHandle) {
				continue;
			}


			$domainParts = explode('.', $domainItemExploded[0]);
			$sld = array_shift($domainParts);
			$tld = implode('.', $domainParts);

			$output[] = new DomainSyncItem(
				$sld,
				$tld,
				array_filter([$domainItemExploded[24], $domainItemExploded[25], $domainItemExploded[26], $domainItemExploded[27], $domainItemExploded[28]]),
				$domainItemExploded[1],
				0,
				Carbon::parse($domainItemExploded[10]),
				Carbon::parse($domainItemExploded[7]),
				$contacts->get($domainItemExploded[20]),
				$contacts->get($domainItemExploded[21]),
				$contacts->get($domainItemExploded[22]),
				$contacts->get($domainItemExploded[23]),
			);
		}

		return $output;
	}

	/**
	 * Helper below
	 */

	private function _getContactFromHandle(array $reportLine) {
		$type = ContactTypeEnum::PERSON;
		if ($reportLine[4] === "organisation") {
			$type = ContactTypeEnum::ORG;
		} elseif ($reportLine[4] === "role") {
			$type = ContactTypeEnum::ROLE;
		}

		return new DomainContact([
			"id" => $reportLine[2],
			"type" => $type,
			"sex" => ContactSexEnum::MALE,
			"first_name" => $reportLine[5],
			"last_name" => $reportLine[6],
			"organisation" => $reportLine[7],
			"street" => $reportLine[8],
			"zipcode" => $reportLine[9],
			"city" => $reportLine[10],
			"state" => $reportLine[11],
			"country" => $reportLine[12],
			"telephone" => $reportLine[13],
			"telefax" => $reportLine[14],
			"email" => $reportLine[15]
		]);
	}


	/**
	 * @param $salesChannelId
	 * @return ApiClient
	 */
	public function getClient() {
		if (!isset(self::$cps)) {
			self::$cps = new ApiClient();
		}
		return self::$cps;
	}

	private function _getHandleFromContact(DomainContact $domainContact) {
		$domainContact->refresh();

		if ($domainContact->getModuleHandle('HwCpsDatensysteme') !== null) {
			return $domainContact->getModuleHandle('HwCpsDatensysteme');
		}

		// lumaserv requires another name for persons
		$type = "PERS";
		if ($domainContact->type !== ContactTypeEnum::PERSON) {
			$type = $domainContact->type->name;
		}

		$output = $this->getClient($domainContact->sales_channel_id)->domains()->handles()->create(
			$type,
			$domainContact->sex->name,
			$domainContact->first_name,
			$domainContact->last_name,
			$domainContact->organisation,
			$domainContact->street,
			$domainContact->street_number,
			$domainContact->zipcode,
			$domainContact->city,
			$domainContact->state,
			$domainContact->country,
			$domainContact->telephone ?? "+49.0000000",
			$domainContact->telefax,
			$domainContact->email,
		);

		if (!$output->success)
			throw new CouldNotCreateDomainContact($output->errors[0]);

		$domainContact->addModuleHandle('HwCpsDatensysteme', $output->data->handle);

		return $output->data->handle;
	}

	public function createHandle(DomainContact $contact) {

		$xml = <<<XML
<?xml version="1.0" encoding="utf-8" ?>
<request>
	{$this->getXmlHeader()}
    <transaction>
        <group>contact</group>
        <action>create</action>
        <attribute>contact</attribute>
        <object>%%AUTO%%</object>
		<values>
			{$this->getXmlContactStructure($contact)}
        </values>
    </transaction>
</request>
XML;

		$response = $this->getClient()->callRaw($xml, $this->config['sandbox']);

		return ((array)$response->result->auto_values->contact_id)[0];
	}

	public function updateHandle(DomainContact $contact) {
		$handle = $contact->getModuleHandle($this->customFieldsKey);

		$xml = <<<XML
<?xml version="1.0" encoding="utf-8" ?>
<request>
	{$this->getXmlHeader()}
    <transaction>
        <group>contact</group>
        <action>modify</action>
        <attribute>replacement</attribute>
        <object>{$handle}:%%AUTO%%</object>
		<values>
			{$this->getXmlContactStructure($contact)}
        </values>
    </transaction>
</request>
XML;

		$response = $this->getClient()->callRaw($xml, $this->config['sandbox']);

		try {
			$newHandleId = ((array)$response->result->auto_values->contact_id)[0];

			$contact->update([
				"modules" => array_merge($contact->modules, [
					$this->customFieldsKey => $newHandleId
				]),
			]);
		} catch (Exception|Throwable $e) {
			Log::error($e->getMessage());
		}

	}

	public function getXmlHeader() {
		return <<<XML
					<auth>
						<cid>{$this->config['cnumber']}</cid>
						<user>{$this->config['user']}</user>
						<pwd>{$this->config['password']}</pwd>
					</auth>
				XML;
	}
	function _formatiere_telefonnummer($telefonnummer, DomainContact $domainContact) {

		$phoneLib = PhoneNumberUtil::getInstance();

		// Input e.g. "+4915151150554"
		if (PhoneNumberUtil::isViablePhoneNumber($telefonnummer)) {
			$parsedNumber = $phoneLib->parse($telefonnummer, $domainContact->country);

			// This returns "tel:+49-1515-1150554"
			$nummer = $phoneLib->format($parsedNumber, PhoneNumberFormat::RFC3966);

			// Remove the library prefix, value "+49-1515-1150554"
			$nummer = str_replace("tel:", "", $nummer);

			// Replace the first dash with a dot, value after: "+49.1515-1150554"
			$pos = strpos($nummer, "-");
			if ($pos !== false) {
				$nummer = substr_replace($nummer, ".", $pos, strlen("-"));
			}

			// Remove the second dash, value after: "+49.15151150554"
			$nummer = str_replace("-", "", $nummer);

			// Finished
			return $nummer;
		}

		return $telefonnummer;
	}

	private function getXmlContactStructure(DomainContact $contact) {
		$type = $contact->type == ContactTypeEnum::PERSON ? 'person' : 'organisation';
		$phone = $contact->telephone ? $this->_formatiere_telefonnummer($contact->telephone, $contact) : "+49.00000000";
		$org = htmlspecialchars($this->germanAscii($contact->organisation ?? "")); // Prevents "XML validity error - encoding is invalid"
		$lastName = $this->germanAscii($contact->last_name);
		$firstName = $this->germanAscii($contact->first_name);
		$street = $this->germanAscii($contact->street_full);
		$postal = $contact->zipcode;
		$city = $this->germanAscii($contact->city);
		$state = $this->clean($this->germanAscii($contact->state));
		$country = $contact->country;
		$fax = $contact->telefax ? "<fax>" .$this->_formatiere_telefonnummer($contact->telefax, $contact) . "</fax>" : null;
//		$fax = $contact->telefax;
		$email = $contact->email;

		if(isset($this->config) && isset($this->config['overwriteContactEmail']) && $this->config['overwriteContactEmail'] !== "") {
			$email = $this->config['overwriteContactEmail'];
		}

		$org = str_replace("&amp", "und", $org);
		$org = preg_replace('/[^-A-Za-z0-9\.\,\&\+\/\(\) ]+/', '', $org);
		$firstName = preg_replace('/[^-A-Za-z0-9\. ]+/', '', $firstName);
		$lastName = preg_replace('/[^-A-Za-z0-9\. ]+/', '', $lastName);

		return "<lastname>{$lastName}</lastname>
			<firstname>{$firstName}</firstname>
			<orgname>{$org}</orgname>
			<street>{$street}</street>
			<postal>{$postal}</postal>
			<city>{$city}</city>
			<state>{$state}</state>
			<iso_country>{$country}</iso_country>
			<phone>{$phone}</phone>
			{$fax}
			<email>{$email}</email>
			<contact_type>{$type}</contact_type>";
	}

	public function clean($string) {
		$string = str_replace(' ', '-', $string); // Replaces all spaces with hyphens.

		return preg_replace('/[^A-Za-z0-9\-]/', '', $string); // Removes special chars.
	}

	private function germanAscii($string) {
		$string = str_replace(
			['ä','ö','ü','Ä','Ö','Ü','ß'],
			['ae','oe','ue','Ae','Oe','Ue','ss'],
			$string
		);

		return Str::ascii($string);
	}
}

```
