|
|
@@ -0,0 +1,191 @@
|
|
|
1
|
+<?php
|
|
|
2
|
+
|
|
|
3
|
+namespace App\Services;
|
|
|
4
|
+
|
|
|
5
|
+use App\Models\Config;
|
|
|
6
|
+use App\Models\Fattura;
|
|
|
7
|
+use App\Models\FatturaRiga;
|
|
|
8
|
+use App\Models\FicCollegamento;
|
|
|
9
|
+use App\Models\Prodotto;
|
|
|
10
|
+use Carbon\Carbon;
|
|
|
11
|
+use RuntimeException;
|
|
|
12
|
+
|
|
|
13
|
+class FatturaFicService
|
|
|
14
|
+{
|
|
|
15
|
+ public function __construct(protected FattureInCloudClient $fic) {}
|
|
|
16
|
+
|
|
|
17
|
+ public function invia(Fattura $fattura): string
|
|
|
18
|
+ {
|
|
|
19
|
+ $fattura->loadMissing(['righe', 'user']);
|
|
|
20
|
+ if ($fattura->isAnnullata()) {
|
|
|
21
|
+ throw new RuntimeException('Una fattura annullata non si invia.');
|
|
|
22
|
+ }
|
|
|
23
|
+ if ($fattura->ficDocumentId()) {
|
|
|
24
|
+ throw new RuntimeException('Questa fattura è già su Fatture in Cloud.');
|
|
|
25
|
+ }
|
|
|
26
|
+ if ($fattura->righe->isEmpty()) {
|
|
|
27
|
+ throw new RuntimeException('La fattura non ha righe.');
|
|
|
28
|
+ }
|
|
|
29
|
+
|
|
|
30
|
+ $collegamento = $this->collegamento($fattura);
|
|
|
31
|
+ if (! $collegamento) {
|
|
|
32
|
+ throw new RuntimeException('Collega il cliente a Fatture in Cloud dalla scheda azienda, poi riprova.');
|
|
|
33
|
+ }
|
|
|
34
|
+
|
|
|
35
|
+ try {
|
|
|
36
|
+ $client = $this->fic->getClient((int) $collegamento->fic_client_id);
|
|
|
37
|
+ $vatId = $this->vatId((float) ($fattura->iva_percentuale ?? Config::ivaPercentualeDefault()));
|
|
|
38
|
+ $document = $this->fic->createIssuedDocument($this->payload($fattura, $client, $vatId));
|
|
|
39
|
+ if (! isset($document['number']) || $document['number'] === '' || $document['number'] === null) {
|
|
|
40
|
+ $document = $this->fic->getIssuedDocument((int) $document['id']);
|
|
|
41
|
+ }
|
|
|
42
|
+ } catch (RuntimeException $e) {
|
|
|
43
|
+ throw new RuntimeException($this->conScope($e->getMessage()));
|
|
|
44
|
+ }
|
|
|
45
|
+
|
|
|
46
|
+ $numeroFic = (int) $document['number'];
|
|
|
47
|
+ if ($numeroFic < 1) {
|
|
|
48
|
+ throw new RuntimeException('Fatture in Cloud ha creato il documento ma non ha assegnato un numero.');
|
|
|
49
|
+ }
|
|
|
50
|
+
|
|
|
51
|
+ $meta = is_array($fattura->meta) ? $fattura->meta : [];
|
|
|
52
|
+ $meta['fic'] = [
|
|
|
53
|
+ 'document_id' => (int) $document['id'],
|
|
|
54
|
+ 'number' => $numeroFic,
|
|
|
55
|
+ 'numeration' => $document['numeration'] ?? null,
|
|
|
56
|
+ 'date' => isset($document['date']) ? (string) $document['date'] : null,
|
|
|
57
|
+ 'sent_at' => now()->toIso8601String(),
|
|
|
58
|
+ ];
|
|
|
59
|
+ $fattura->meta = $meta;
|
|
|
60
|
+ $fattura->numero = $numeroFic;
|
|
|
61
|
+ if (! empty($document['date'])) {
|
|
|
62
|
+ $fattura->data = Carbon::parse($document['date'])->toDateString();
|
|
|
63
|
+ }
|
|
|
64
|
+ if ($fattura->isBozza()) {
|
|
|
65
|
+ $fattura->stato = Fattura::STATO_EMESSA;
|
|
|
66
|
+ if (! $fattura->data) {
|
|
|
67
|
+ $fattura->data = now()->toDateString();
|
|
|
68
|
+ }
|
|
|
69
|
+ }
|
|
|
70
|
+ $fattura->save();
|
|
|
71
|
+
|
|
|
72
|
+ return 'Fattura '.$fattura->numero_documento.' allineata a Fatture in Cloud. L\'invio allo SDI lo fai da lì.';
|
|
|
73
|
+ }
|
|
|
74
|
+
|
|
|
75
|
+ protected function collegamento(Fattura $fattura): ?FicCollegamento
|
|
|
76
|
+ {
|
|
|
77
|
+ $ids = array_values(array_filter([
|
|
|
78
|
+ (int) $fattura->user_id,
|
|
|
79
|
+ (int) ($fattura->user->azienda_id ?? 0),
|
|
|
80
|
+ ]));
|
|
|
81
|
+
|
|
|
82
|
+ return FicCollegamento::query()->whereIn('user_id', $ids)->first();
|
|
|
83
|
+ }
|
|
|
84
|
+
|
|
|
85
|
+ /**
|
|
|
86
|
+ * @param array<string, mixed> $client
|
|
|
87
|
+ * @return array<string, mixed>
|
|
|
88
|
+ */
|
|
|
89
|
+ protected function payload(Fattura $fattura, array $client, int $vatId): array
|
|
|
90
|
+ {
|
|
|
91
|
+ $data = $fattura->data ? Carbon::parse($fattura->data)->toDateString() : now()->toDateString();
|
|
|
92
|
+ $entity = ['id' => (int) $client['id']];
|
|
|
93
|
+ foreach ([
|
|
|
94
|
+ 'name',
|
|
|
95
|
+ 'vat_number',
|
|
|
96
|
+ 'tax_code',
|
|
|
97
|
+ 'address_street',
|
|
|
98
|
+ 'address_postal_code',
|
|
|
99
|
+ 'address_city',
|
|
|
100
|
+ 'address_province',
|
|
|
101
|
+ 'country',
|
|
|
102
|
+ 'email',
|
|
|
103
|
+ ] as $field) {
|
|
|
104
|
+ $value = trim((string) ($client[$field] ?? ''));
|
|
|
105
|
+ if ($value !== '') {
|
|
|
106
|
+ $entity[$field] = $value;
|
|
|
107
|
+ }
|
|
|
108
|
+ }
|
|
|
109
|
+ if (empty($entity['name'])) {
|
|
|
110
|
+ $entity['name'] = $fattura->ragione_sociale ?: 'Cliente';
|
|
|
111
|
+ }
|
|
|
112
|
+
|
|
|
113
|
+ $items = [];
|
|
|
114
|
+ foreach ($fattura->righe as $riga) {
|
|
|
115
|
+ $items[] = $this->item($riga, $vatId);
|
|
|
116
|
+ }
|
|
|
117
|
+
|
|
|
118
|
+ $payload = [
|
|
|
119
|
+ 'type' => 'invoice',
|
|
|
120
|
+ 'entity' => $entity,
|
|
|
121
|
+ 'date' => $data,
|
|
|
122
|
+ 'items_list' => $items,
|
|
|
123
|
+ 'e_invoice' => false,
|
|
|
124
|
+ ];
|
|
|
125
|
+
|
|
|
126
|
+ if ($fattura->periodo_da && $fattura->periodo_a) {
|
|
|
127
|
+ $payload['visible_subject'] = 'Periodo '
|
|
|
128
|
+ .Carbon::parse($fattura->periodo_da)->format('d/m/Y')
|
|
|
129
|
+ .' – '
|
|
|
130
|
+ .Carbon::parse($fattura->periodo_a)->format('d/m/Y');
|
|
|
131
|
+ }
|
|
|
132
|
+
|
|
|
133
|
+ return $payload;
|
|
|
134
|
+ }
|
|
|
135
|
+
|
|
|
136
|
+ /**
|
|
|
137
|
+ * @return array<string, mixed>
|
|
|
138
|
+ */
|
|
|
139
|
+ protected function item(FatturaRiga $riga, int $vatId): array
|
|
|
140
|
+ {
|
|
|
141
|
+ $qty = (float) ($riga->quantita ?? 1);
|
|
|
142
|
+ if ($qty <= 0) {
|
|
|
143
|
+ $qty = 1;
|
|
|
144
|
+ }
|
|
|
145
|
+ $name = trim((string) $riga->descrizione);
|
|
|
146
|
+ if ($name === '') {
|
|
|
147
|
+ $name = $riga->tipo_label;
|
|
|
148
|
+ }
|
|
|
149
|
+ if (mb_strlen($name) > 255) {
|
|
|
150
|
+ $name = mb_substr($name, 0, 252).'…';
|
|
|
151
|
+ }
|
|
|
152
|
+
|
|
|
153
|
+ $item = [
|
|
|
154
|
+ 'name' => $name,
|
|
|
155
|
+ 'qty' => $qty,
|
|
|
156
|
+ 'net_price' => round((float) ($riga->importo ?? 0) / $qty, 5),
|
|
|
157
|
+ 'vat' => ['id' => $vatId],
|
|
|
158
|
+ ];
|
|
|
159
|
+ $measure = Prodotto::UNITA_MISURA[$riga->unita_misura] ?? $riga->unita_misura;
|
|
|
160
|
+ if (filled($measure)) {
|
|
|
161
|
+ $item['measure'] = mb_substr((string) $measure, 0, 20);
|
|
|
162
|
+ }
|
|
|
163
|
+
|
|
|
164
|
+ return $item;
|
|
|
165
|
+ }
|
|
|
166
|
+
|
|
|
167
|
+ protected function vatId(float $percent): int
|
|
|
168
|
+ {
|
|
|
169
|
+ foreach ($this->fic->vatTypes() as $type) {
|
|
|
170
|
+ if (! is_array($type) || ! empty($type['is_disabled'])) {
|
|
|
171
|
+ continue;
|
|
|
172
|
+ }
|
|
|
173
|
+ if (abs((float) ($type['value'] ?? -1) - $percent) < 0.001 && isset($type['id'])) {
|
|
|
174
|
+ return (int) $type['id'];
|
|
|
175
|
+ }
|
|
|
176
|
+ }
|
|
|
177
|
+
|
|
|
178
|
+ $label = rtrim(rtrim(number_format($percent, 2, ',', ''), '0'), ',');
|
|
|
179
|
+
|
|
|
180
|
+ throw new RuntimeException('Su Fatture in Cloud non c\'è un\'aliquota IVA del '.$label.'%.');
|
|
|
181
|
+ }
|
|
|
182
|
+
|
|
|
183
|
+ protected function conScope(string $message): string
|
|
|
184
|
+ {
|
|
|
185
|
+ if (preg_match('/403|permission|permess|scope/i', $message)) {
|
|
|
186
|
+ return $message.' Ricrea il token con anche gli scope settings:r e issued_documents.invoices:a.';
|
|
|
187
|
+ }
|
|
|
188
|
+
|
|
|
189
|
+ return $message;
|
|
|
190
|
+ }
|
|
|
191
|
+}
|