diff --git a/README.md b/README.md
index 021fa1a..b41d78f 100644
--- a/README.md
+++ b/README.md
@@ -55,6 +55,11 @@ Currently the following endpoints are covered:
- [x] Get unsubscribed contacts since date
- [x] Unsubscribe contact
- [x] Resubscribe contact
+ - [x] Resubscribe contact with no challenge
+ - [x] Get suppressed contacts since date
+ - [x] Bulk create contacts in address book
+ - [x] Get contact import status
+ - [x] Get contact import report
- [ ] **Contact data fields**
- [x] Create contact data field
- [x] Delete contact data field
diff --git a/phpmd.xml b/phpmd.xml
index a3d0c9c..b2245bf 100644
--- a/phpmd.xml
+++ b/phpmd.xml
@@ -7,6 +7,8 @@
xsi:noNamespaceSchemaLocation="
http://pmd.sf.net/ruleset_xml_schema.xsd">
+
+
diff --git a/src/Adapter/Adapter.php b/src/Adapter/Adapter.php
index c242e7e..f7f0d29 100644
--- a/src/Adapter/Adapter.php
+++ b/src/Adapter/Adapter.php
@@ -36,4 +36,12 @@ public function put(string $url, array $content = []): ResponseInterface;
* @return ResponseInterface
*/
public function delete(string $url): ResponseInterface;
+
+ /**
+ * @param string $url
+ * @param string $filename
+ *
+ * @return ResponseInterface
+ */
+ public function postfile(string $url, string $filePath, string $fileName, string $mimeType): ResponseInterface;
}
diff --git a/src/Adapter/GuzzleAdapter.php b/src/Adapter/GuzzleAdapter.php
index 221c259..c6f6616 100644
--- a/src/Adapter/GuzzleAdapter.php
+++ b/src/Adapter/GuzzleAdapter.php
@@ -82,4 +82,23 @@ public function delete(string $url): ResponseInterface
{
return $this->client->request('DELETE', $url);
}
+
+ /**
+ * {@inheritDoc}
+ */
+ public function postfile(string $url, string $filePath, string $fileName, string $mimeType): ResponseInterface
+ {
+ return $this->client->request('POST', $url, [
+ 'multipart' => [
+ [
+ 'name' => 'file',
+ 'contents' => fopen($filePath, 'r'), // Just the resource; Guzzle handles the contents internally
+ 'filename' => $fileName,
+ 'headers' => [
+ 'Content-Type' => $mimeType
+ ]
+ ]
+ ]
+ ]);
+ }
}
diff --git a/src/Dotmailer.php b/src/Dotmailer.php
index 036d4f2..ec0f214 100644
--- a/src/Dotmailer.php
+++ b/src/Dotmailer.php
@@ -11,10 +11,14 @@
use Dotmailer\Factory\CampaignFactory;
use Psr\Http\Message\ResponseInterface;
use function GuzzleHttp\json_decode;
+use Dotmailer\Entity\Suppression;
+use Dotmailer\Entity\ContactImportStatus;
+use Dotmailer\Entity\ContactImportReport;
class Dotmailer
{
const DEFAULT_URI = 'https://r1-api.dotmailer.com';
+ const GUID_REGEX = '/^[A-Z0-9]{8}-[A-Z0-9]{4}-[A-Z0-9]{4}-[A-Z0-9]{4}-[A-Z0-9]{12}?$/i';
/**
* @var Adapter
@@ -161,7 +165,8 @@ public function getContactByEmail(string $email): Contact
$contact->email,
$contact->optInType,
$contact->emailType,
- $contact->dataFields
+ $contact->dataFields,
+ $contact->status
);
}
@@ -251,6 +256,22 @@ public function resubscribeContact(Contact $contact, string $preferredLocale = n
$this->response = $this->adapter->post('/v2/contacts/resubscribe', array_filter($content));
}
+
+ /**
+ * @param Contact $contact
+ * @param string|null $preferredLocale
+ * @param string|null $challengeUrl
+ */
+ public function resubscribeContactWithNoChallenge(Contact $contact)
+ {
+ $content = [
+ 'unsubscribedContact' => [
+ 'email' => $contact->getEmail()
+ ],
+ ];
+
+ $this->response = $this->adapter->post('/v2/contacts/resubscribe-with-no-challenge', array_filter($content));
+ }
/**
* @param DataField $dataField
@@ -375,4 +396,105 @@ function (string $name, string $value) {
]
);
}
+
+ /**
+ * @param \DateTimeInterface $dateTime
+ * @param int|null $select
+ * @param int|null $skip
+ *
+ * @return Suppression[]
+ */
+ public function getSuppressedContactsSince(
+ \DateTimeInterface $dateTime,
+ int $select = null,
+ int $skip = null
+ ): array {
+ $this->response = $this->adapter->get(
+ '/v2/contacts/suppressed-since/' . $dateTime->format('Y-m-d'),
+ array_filter([
+ 'select' => $select,
+ 'skip' => $skip,
+ ])
+ );
+
+ $suppressions = [];
+
+ foreach (json_decode($this->response->getBody()->getContents()) as $suppression) {
+ $suppressions[] = new Suppression(
+ new Contact(
+ $suppression->suppressedContact->id,
+ $suppression->suppressedContact->email,
+ $suppression->suppressedContact->optInType,
+ $suppression->suppressedContact->emailType
+ ),
+ new \DateTime($suppression->dateRemoved),
+ $suppression->reason
+ );
+ }
+
+ return $suppressions;
+ }
+
+ /**
+ * Bulk creates, or bulk updates, contacts in an address book
+ *
+ * @param AddressBook $addressBook Object containing the ID of the address book
+ * @param string $filePath Local filesystem path of the file to be imported
+ * @param string $fileName Discrete file name to pass to API
+ *
+ * @return \Dotmailer\Entity\ContactImportStatus
+ */
+ public function bulkCreateContactsInAddressBook(AddressBook $addressBook, string $filePath, string $fileName)
+ {
+ $this->response = $this->adapter->postfile(
+ '/v2/address-books/' . $addressBook->getId() . '/contacts/import',
+ $filePath,
+ $fileName,
+ 'text/csv'
+ );
+
+ $response = json_decode($this->response->getBody()->getContents());
+
+ $importStatus = new ContactImportStatus($response->id, $response->status);
+
+ return $importStatus;
+ }
+
+ /**
+ * @param string $id GUID import ID
+ *
+ * @return ContactImportStatus
+ */
+ public function getContactImportStatus(string $id): ContactImportStatus
+ {
+ if (!preg_match(self::GUID_REGEX, $id)) {
+ throw new \Exception('ID did not contain a valid GUID');
+ }
+
+ $this->response = $this->adapter->get('/v2/contacts/import/' . $id);
+
+ $response = json_decode($this->response->getBody()->getContents());
+
+ $importStatus = new ContactImportStatus($response->id, $response->status);
+
+ return $importStatus;
+ }
+
+ /**
+ * @param string $id GUID import ID
+ *
+ * @return ContactImportReport
+ */
+ public function getContactImportReport(string $id): ContactImportReport
+ {
+ if (!preg_match(self::GUID_REGEX, $id)) {
+ throw new \Exception('ID did not contain a valid GUID');
+ }
+
+ $this->response = $this->adapter->get('/v2/contacts/import/' . $id . '/report');
+
+ $report = ContactImportReport::fromJson($this->response->getBody()->getContents());
+
+ return $report;
+ }
}
diff --git a/src/Entity/Contact.php b/src/Entity/Contact.php
index 71f3e03..cd7b742 100644
--- a/src/Entity/Contact.php
+++ b/src/Entity/Contact.php
@@ -37,6 +37,11 @@ final class Contact implements Arrayable
*/
private $dataFields;
+ /**
+ * @var string
+ */
+ private $status;
+
/**
* @param int|null $id
* @param string $email
@@ -49,13 +54,15 @@ public function __construct(
string $email,
string $optInType = self::OPT_IN_TYPE_UNKNOWN,
string $emailType = self::EMAIL_TYPE_PLAIN_TEXT,
- array $dataFields = []
+ array $dataFields = [],
+ string $status = null
) {
$this->id = $id;
$this->email = $email;
$this->optInType = $optInType;
$this->emailType = $emailType;
$this->dataFields = $dataFields;
+ $this->status = $status;
}
/**
@@ -98,6 +105,14 @@ public function getEmailType(): string
return $this->emailType;
}
+ /**
+ * @return string
+ */
+ public function getStatus(): string
+ {
+ return $this->status;
+ }
+
/**
* @return array
*/
@@ -161,6 +176,7 @@ public function asArray(): array
'optInType' => $this->optInType,
'emailType' => $this->emailType,
'dataFields' => $this->dataFields,
+ 'status' => $this->status,
];
}
}
diff --git a/src/Entity/ContactImportReport.php b/src/Entity/ContactImportReport.php
new file mode 100644
index 0000000..0c7d62b
--- /dev/null
+++ b/src/Entity/ContactImportReport.php
@@ -0,0 +1,119 @@
+newContacts = $newContacts;
+ $this->updatedContacts = $updatedContacts;
+ $this->globallySuppressed = $globallySuppressed;
+ $this->invalidEntries = $invalidEntries;
+ $this->duplicateEmails = $duplicateEmails;
+ $this->blocked = $blocked;
+ $this->unsubscribed = $unsubscribed;
+ $this->hardBounced = $hardBounced;
+ $this->softBounced = $softBounced;
+ $this->ispComplaints = $ispComplaints;
+ $this->mailBlocked = $mailBlocked;
+ $this->domainSuppressed = $domainSuppressed;
+ $this->pendingDoubleOptin = $pendingDoubleOptin;
+ $this->failures = $failures;
+ }
+
+ /**
+ * Initialise object from JSON
+ *
+ * @param string $json
+ * @return ContactImportReport
+ */
+ public static function fromJson(string $json): self
+ {
+ $result = json_decode($json);
+
+ return new self(
+ $result->newContacts,
+ $result->updatedContacts,
+ $result->globallySuppressed,
+ $result->invalidEntries,
+ $result->duplicateEmails,
+ $result->blocked,
+ $result->unsubscribed,
+ $result->hardBounced,
+ $result->softBounced,
+ $result->ispComplaints,
+ $result->mailBlocked,
+ $result->domainSuppressed,
+ $result->pendingDoubleOptin,
+ $result->failures
+ );
+ }
+
+ /**
+ * @inheritdoc
+ */
+ public function asArray(): array
+ {
+ return [
+ 'newContacts' => $this->newContacts,
+ 'updatedContacts' => $this->updatedContacts,
+ 'globallySuppressed' => $this->globallySuppressed,
+ 'invalidEntries' => $this->invalidEntries,
+ 'duplicateEmails' => $this->duplicateEmails,
+ 'blocked' => $this->blocked,
+ 'unsubscribed' => $this->unsubscribed,
+ 'hardBounced' => $this->hardBounced,
+ 'softBounced' => $this->softBounced,
+ 'ispComplaints' => $this->ispComplaints,
+ 'mailBlocked' => $this->mailBlocked,
+ 'domainSuppressed' => $this->domainSuppressed,
+ 'pendingDoubleOptin' => $this->pendingDoubleOptin,
+ 'failures' => $this->failures,
+ ];
+ }
+}
diff --git a/src/Entity/ContactImportStatus.php b/src/Entity/ContactImportStatus.php
new file mode 100644
index 0000000..1f56ed1
--- /dev/null
+++ b/src/Entity/ContactImportStatus.php
@@ -0,0 +1,95 @@
+id = $id;
+ $this->status = $status;
+ }
+
+ /**
+ * @return string
+ */
+ public function getId(): string
+ {
+ return $this->id;
+ }
+
+ /**
+ * @return string
+ */
+ public function getStatus(): string
+ {
+ return $this->status;
+ }
+
+ /**
+ * @inheritdoc
+ */
+ public function asArray(): array
+ {
+ return [
+ 'id' => $this->id,
+ 'status' => $this->status,
+ ];
+ }
+}
diff --git a/src/Entity/Suppression.php b/src/Entity/Suppression.php
new file mode 100644
index 0000000..7634ba9
--- /dev/null
+++ b/src/Entity/Suppression.php
@@ -0,0 +1,145 @@
+suppressedContact = $suppressedContact;
+ $this->dateRemoved = $dateRemoved;
+ $this->reason = $reason;
+ }
+
+ /**
+ * @return Contact
+ */
+ public function getSuppressedContact(): Contact
+ {
+ return $this->suppressedContact;
+ }
+
+ /**
+ * @return \DateTime
+ */
+ public function getDateRemoved(): \DateTime
+ {
+ return $this->dateRemoved;
+ }
+
+ /**
+ * @return string
+ */
+ public function getReason(): string
+ {
+ return $this->reason;
+ }
+
+ /**
+ * @inheritdoc
+ */
+ public function asArray(): array
+ {
+ return [
+ 'suppressedContact' => $this->suppressedContact,
+ 'dateRemoved' => $this->dateRemoved,
+ 'reason' => $this->reason,
+ ];
+ }
+}
diff --git a/tests/DotmailerTest.php b/tests/DotmailerTest.php
index d068fb8..a3e5d32 100644
--- a/tests/DotmailerTest.php
+++ b/tests/DotmailerTest.php
@@ -14,6 +14,9 @@
use PHPUnit\Framework\MockObject\MockObject;
use PHPUnit\Framework\TestCase;
use function GuzzleHttp\json_encode;
+use Dotmailer\Entity\Suppression;
+use Dotmailer\Entity\ContactImportStatus;
+use Dotmailer\Entity\ContactImportReport;
class DotmailerTest extends TestCase
{
@@ -28,6 +31,10 @@ class DotmailerTest extends TestCase
const WEBSITE = 'http://foo.bar/baz';
const DATA_FIELD = 'DATAFIELD';
const DATE_FROM = '2018-01-01';
+ const NULL_GUID = '00000000-0000-0000-0000-000000000000';
+ const FILE_PATH = '/test.csv';
+ const FILE_NAME = 'test.csv';
+ const FILE_MIME_TYPE = 'text/csv';
/**
* @var Adapter|MockObject
@@ -422,6 +429,93 @@ public function testSendTransactionalEmailUsingATriggeredCampaign()
$this->assertEquals($response, $this->dotmailer->getResponse());
}
+
+ public function testGetSuppressedContactsSince()
+ {
+ $contact = $this->getContact();
+ $dateRemoved = new \DateTime('2018-01-10');
+
+ $this->adapter
+ ->expects($this->once())
+ ->method('get')
+ ->with(
+ '/v2/contacts/suppressed-since/' . self::DATE_FROM,
+ [
+ 'select' => 1,
+ 'skip' => 2,
+ ]
+ )
+ ->willReturn(
+ $this->getResponse(
+ [
+ [
+ 'suppressedContact' => $contact->asArray(),
+ 'dateRemoved' => $dateRemoved->format('Y-m-d'),
+ 'reason' => 'unsubscribed',
+ ]
+ ]
+ )
+ );
+
+ $this->assertEquals(
+ [
+ new Suppression(
+ $contact,
+ $dateRemoved,
+ 'unsubscribed'
+ )
+ ],
+ $this->dotmailer->getSuppressedContactsSince(new \DateTime(self::DATE_FROM), 1, 2)
+ );
+ }
+
+ public function testBulkCreateContactsInAddressBook()
+ {
+ $this->adapter
+ ->expects($this->once())
+ ->method('postfile')
+ ->with(
+ '/v2/address-books/' . self::ID . '/contacts/import',
+ self::FILE_PATH,
+ self::FILE_NAME,
+ self::FILE_MIME_TYPE
+ )
+ ->willReturn($this->getResponse($this->getContactImportStatus()->asArray()));
+
+ $actual = $this->dotmailer->bulkCreateContactsInAddressBook(
+ new AddressBook(self::ID, self::NAME),
+ self::FILE_PATH,
+ self::FILE_NAME
+ );
+
+ $this->assertEquals($this->getContactImportStatus(), $actual);
+ }
+
+ public function testGetContactImportStatus()
+ {
+ $this->adapter
+ ->expects($this->once())
+ ->method('get')
+ ->with('/v2/contacts/import/' . self::NULL_GUID)
+ ->willReturn(
+ $this->getResponse($this->getContactImportStatus()->asArray())
+ );
+
+ $this->assertEquals($this->getContactImportStatus(), $this->dotmailer->getContactImportStatus(self::NULL_GUID));
+ }
+
+ public function testGetContactImportReport()
+ {
+ $this->adapter
+ ->expects($this->once())
+ ->method('get')
+ ->with('/v2/contacts/import/' . self::NULL_GUID . '/report')
+ ->willReturn(
+ $this->getResponse($this->getContactImportReport()->asArray())
+ );
+
+ $this->assertEquals($this->getContactImportReport(), $this->dotmailer->getContactImportReport(self::NULL_GUID));
+ }
/**
* @param array $contents
@@ -478,4 +572,41 @@ private function getProgram(): Program
{
return new Program(self::ID, 'test program', Program::STATUS_ACTIVE, new \DateTime());
}
+
+ /**
+ * @return ContactImportStatus
+ */
+ private function getContactImportStatus(): ContactImportStatus
+ {
+ return new ContactImportStatus(self::NULL_GUID, ContactImportStatus::STATUS_NOT_FINISHED);
+ }
+
+ /**
+ * @return ContactImportReport
+ */
+ private function getContactImportReport(): ContactImportReport
+ {
+ return new ContactImportReport(1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14);
+ }
+
+ public function testResubscribeContactWithNoChallenge()
+ {
+ $response = $this->getResponse();
+
+ $this->adapter
+ ->expects($this->once())
+ ->method('post')
+ ->with(
+ '/v2/contacts/resubscribe-with-no-challenge',
+ [
+ 'unsubscribedContact' => [
+ 'email' => self::EMAIL
+ ],
+ ]
+ )->willReturn($response);
+
+ $this->dotmailer->resubscribeContactWithNoChallenge($this->getContact());
+
+ $this->assertEquals($response, $this->dotmailer->getResponse());
+ }
}