Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
57.89% covered (warning)
57.89%
22 / 38
75.00% covered (warning)
75.00%
3 / 4
CRAP
0.00% covered (danger)
0.00%
0 / 1
PandaDocDocumentData
57.89% covered (warning)
57.89%
22 / 38
75.00% covered (warning)
75.00%
3 / 4
8.69
0.00% covered (danger)
0.00%
0 / 1
 __construct
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 fromPandaDocResponse
0.00% covered (danger)
0.00%
0 / 16
0.00% covered (danger)
0.00%
0 / 1
12
 fromStoredFallback
100.00% covered (success)
100.00%
7 / 7
100.00% covered (success)
100.00%
1 / 1
1
 toArray
100.00% covered (success)
100.00%
14 / 14
100.00% covered (success)
100.00%
1 / 1
1
1<?php
2
3declare(strict_types=1);
4
5namespace App\Domain\Document\Data;
6
7use App\Support\Row;
8
9/**
10 * A PandaDoc document, projected into our domain shape.
11 *
12 * Field-name translation between the PandaDoc API wire format
13 * (snake_case, e.g. `date_status_changed`) and our camelCase fields
14 * happens in {@see fromPandaDocResponse}, so consumers never depend on
15 * PandaDoc's wire shape. The status field always carries the full
16 * PandaDoc form (e.g. `document.sent`) so frontend status mappings
17 * stay aligned with the third-party source of truth.
18 */
19final readonly class PandaDocDocumentData
20{
21    /**
22     * @param list<PandaDocRecipientData> $recipients
23     * @param string $id
24     * @param string $name
25     * @param string $status
26     * @param ?string $dateSent
27     * @param ?string $dateCompleted
28     * @param ?string $expirationDate
29     * @param ?string $createdAt
30     * @param ?string $updatedAt
31     */
32    public function __construct(
33        public string $id = '',
34        public string $name = '',
35        public string $status = '',
36        public ?string $dateSent = null,
37        public ?string $dateCompleted = null,
38        public ?string $expirationDate = null,
39        public ?string $createdAt = null,
40        public ?string $updatedAt = null,
41        public array $recipients = [],
42    ) {}
43
44    /**
45     * Build from a PandaDoc API document object.
46     *
47     * @param array<mixed> $doc
48     */
49    public static function fromPandaDocResponse(array $doc): self
50    {
51        $recipients = [];
52        $rawRecipients = Row::nullableArray($doc, 'recipients') ?? [];
53        foreach ($rawRecipients as $recipient) {
54            if (is_array($recipient)) {
55                $recipients[] = PandaDocRecipientData::fromPandaDocResponse($recipient);
56            }
57        }
58
59        return new self(
60            id: Row::nullableString($doc, 'id') ?? '',
61            name: Row::nullableString($doc, 'name') ?? '',
62            status: Row::nullableString($doc, 'status') ?? '',
63            dateSent: Row::nullableString($doc, 'date_status_changed'),
64            dateCompleted: Row::nullableString($doc, 'date_completed'),
65            expirationDate: Row::nullableString($doc, 'expiration_date'),
66            createdAt: Row::nullableString($doc, 'date_created'),
67            updatedAt: Row::nullableString($doc, 'date_modified'),
68            recipients: $recipients,
69        );
70    }
71
72    /**
73     * Build a fallback document from locally-stored metadata when the
74     * PandaDoc API is unreachable. The status here is already in the full
75     * PandaDoc form because the database stores it that way.
76     * @param string $pandadocId
77     * @param string $documentName
78     * @param string $status
79     * @param ?string $createdAt
80     * @param ?string $updatedAt
81     */
82    public static function fromStoredFallback(
83        string $pandadocId,
84        string $documentName,
85        string $status,
86        ?string $createdAt,
87        ?string $updatedAt,
88    ): self {
89        return new self(
90            id: $pandadocId,
91            name: $documentName,
92            status: $status,
93            createdAt: $createdAt,
94            updatedAt: $updatedAt,
95        );
96    }
97
98    /**
99     * Serialize for the JSON response. Nulls are kept (not filtered) so the
100     * frontend's `string | null` fields receive an explicit null rather than
101     * undefined.
102     *
103     * @return array<string, mixed>
104     */
105    public function toArray(): array
106    {
107        return [
108            'id' => $this->id,
109            'name' => $this->name,
110            'status' => $this->status,
111            'dateSent' => $this->dateSent,
112            'dateCompleted' => $this->dateCompleted,
113            'expirationDate' => $this->expirationDate,
114            'createdAt' => $this->createdAt,
115            'updatedAt' => $this->updatedAt,
116            'recipients' => array_map(
117                static fn(PandaDocRecipientData $r): array => $r->toArray(),
118                $this->recipients,
119            ),
120        ];
121    }
122}