Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
98.67% covered (success)
98.67%
74 / 75
66.67% covered (warning)
66.67%
2 / 3
CRAP
0.00% covered (danger)
0.00%
0 / 1
CreateDocumentAction
98.67% covered (success)
98.67%
74 / 75
66.67% covered (warning)
66.67%
2 / 3
12
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
 execute
98.63% covered (success)
98.63%
72 / 73
0.00% covered (danger)
0.00%
0 / 1
10
 isProposalType
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
1<?php
2
3declare(strict_types=1);
4
5namespace ImovelZapAi\Documents\Application\Actions;
6
7use App\Models\User;
8use Illuminate\Support\Facades\DB;
9use Illuminate\Support\Str;
10use ImovelZapAi\Conversations\Infrastructure\Persistence\Eloquent\ContactModel;
11use ImovelZapAi\Documents\Application\Services\DefaultDocumentTemplateSeeder;
12use ImovelZapAi\Documents\Application\Services\DocumentHistoryWriter;
13use ImovelZapAi\Documents\Application\Services\DocumentPdfGenerator;
14use ImovelZapAi\Documents\Application\Services\DocumentVariableResolver;
15use ImovelZapAi\Documents\Domain\Enums\DocumentHistoryEvent;
16use ImovelZapAi\Documents\Domain\Enums\DocumentStatus;
17use ImovelZapAi\Documents\Domain\Enums\DocumentType;
18use ImovelZapAi\Documents\Domain\Events\DocumentCreated;
19use ImovelZapAi\Documents\Domain\Exceptions\DocumentException;
20use ImovelZapAi\Documents\Infrastructure\Persistence\Eloquent\BusinessDocumentModel;
21use ImovelZapAi\Documents\Infrastructure\Persistence\Eloquent\DocumentTemplateModel;
22use ImovelZapAi\Documents\Infrastructure\Persistence\Eloquent\DocumentVersionModel;
23use ImovelZapAi\Properties\Infrastructure\Persistence\Eloquent\PropertyModel;
24use ImovelZapAi\Sales\Application\Actions\ChangeDealStageAction;
25use ImovelZapAi\Sales\Domain\Enums\DealStageKey;
26use ImovelZapAi\Sales\Infrastructure\Persistence\Eloquent\DealModel;
27
28final class CreateDocumentAction
29{
30    public function __construct(
31        private readonly DefaultDocumentTemplateSeeder $templates,
32        private readonly DocumentVariableResolver $resolver,
33        private readonly DocumentPdfGenerator $pdf,
34        private readonly DocumentHistoryWriter $history,
35        private readonly ChangeDealStageAction $changeDealStage,
36    ) {}
37
38    /**
39     * @param  array{
40     *     contact_id: string,
41     *     deal_id?: string|null,
42     *     property_id?: string|null,
43     *     owner_id?: string|null,
44     *     template_id?: string|null,
45     *     document_type?: string|null,
46     *     title?: string|null,
47     *     amount?: float|string|null,
48     *     intro_message?: string|null,
49     *     expires_at?: string|null,
50     *     client_portal_id?: string|null,
51     *     context_snapshot?: array<string, mixed>|null,
52     * }  $data
53     */
54    public function execute(string $tenantId, User $actor, array $data): BusinessDocumentModel
55    {
56        return DB::transaction(function () use ($tenantId, $actor, $data): BusinessDocumentModel {
57            $contact = ContactModel::query()->where('tenant_id', $tenantId)->whereKey($data['contact_id'])->firstOrFail();
58
59            $deal = null;
60            if (! empty($data['deal_id'])) {
61                $deal = DealModel::query()->where('tenant_id', $tenantId)->whereKey($data['deal_id'])->firstOrFail();
62            }
63
64            $propertyId = $data['property_id'] ?? $deal?->property_id;
65            if ($propertyId !== null) {
66                PropertyModel::query()->where('tenant_id', $tenantId)->whereKey($propertyId)->firstOrFail();
67            }
68
69            $template = ! empty($data['template_id'])
70                ? DocumentTemplateModel::query()->where('tenant_id', $tenantId)->whereKey($data['template_id'])->firstOrFail()
71                : (! empty($data['document_type'])
72                    ? $this->templates->defaultForType($tenantId, $data['document_type'], $actor)
73                    : throw DocumentException::invalidState('informe template_id ou document_type.'));
74
75            $documentType = $template->document_type;
76            $number = 'D-'.now()->format('ymd').'-'.Str::upper(Str::random(4));
77            $title = $data['title'] ?? ($documentType->label().' — '.($contact->display_name ?: 'Cliente'));
78
79            $document = BusinessDocumentModel::query()->create([
80                'tenant_id' => $tenantId,
81                'document_type' => $documentType,
82                'deal_id' => $deal?->id,
83                'contact_id' => $contact->id,
84                'property_id' => $propertyId,
85                'owner_id' => $data['owner_id'] ?? null,
86                'template_id' => $template->id,
87                'client_portal_id' => $data['client_portal_id'] ?? null,
88                'number' => $number,
89                'title' => $title,
90                'status' => DocumentStatus::Draft,
91                'amount' => $data['amount'] ?? $deal?->amount,
92                'currency' => 'BRL',
93                'intro_message' => $data['intro_message'] ?? 'Segue documento para sua análise.',
94                'context_snapshot' => $data['context_snapshot'] ?? null,
95                'expires_at' => $data['expires_at'] ?? now()->addDays((int) config('documents.default_expiry_days', 7)),
96                'created_by' => $actor->id,
97                'responsible_user_id' => $actor->id,
98            ]);
99
100            $document->load(['contact', 'property.neighborhood', 'property.city', 'responsible', 'template']);
101            $snapshot = $this->resolver->snapshot($document, 1);
102            $html = $this->resolver->resolve($template->body_html, $snapshot['variables']);
103
104            $version = DocumentVersionModel::query()->create([
105                'tenant_id' => $tenantId,
106                'business_document_id' => $document->id,
107                'version' => 1,
108                'snapshot' => $snapshot,
109                'html_body' => $html,
110                'change_summary' => 'Versão inicial',
111                'validation_code' => Str::upper(Str::random(10)),
112                'created_by' => $actor->id,
113            ]);
114
115            $pdf = $this->pdf->generate($document, $version, '');
116            $version->forceFill(['pdf_disk' => $pdf['disk'], 'pdf_path' => $pdf['path']])->save();
117
118            $document->forceFill(['current_version_id' => $version->id])->save();
119
120            $this->history->write(
121                $tenantId,
122                $document->id,
123                DocumentHistoryEvent::Created,
124                actorType: 'broker',
125                actorUserId: (int) $actor->id,
126                message: 'Documento criado.',
127            );
128            $this->history->write(
129                $tenantId,
130                $document->id,
131                DocumentHistoryEvent::PdfGenerated,
132                actorType: 'system',
133                message: 'PDF da v1 gerado.',
134            );
135
136            if ($deal !== null && ! $deal->isClosed() && $this->isProposalType($documentType)) {
137                try {
138                    $this->changeDealStage->execute($deal, $actor, DealStageKey::Proposal->value);
139                } catch (\Throwable) {
140                    // deal already in terminal or same stage
141                }
142            }
143
144            DocumentCreated::dispatch($tenantId, $document->id);
145
146            return $document->fresh(['currentVersion', 'contact', 'property', 'deal', 'template']);
147        });
148    }
149
150    private function isProposalType(DocumentType $type): bool
151    {
152        return in_array($type, [DocumentType::ProposalSale, DocumentType::ProposalRent], true);
153    }
154}